Kotlin from the Ground Up (Part 4)

Mike Warner
14 min readJul 10, 2020

--

Click here to go back to the start of the series where you can view the TOC.

What does this part cover:

  1. Starting a new project with Gradle
  2. Crafting a Gradle build file
  3. More Gradle resources

Starting a new project with Gradle

What are we building?

We’ll build a simple app that can control Philips Hue Lights, if you don’t own a set, don’t worry, we will also implement a NOOP solution which prints out the status of the lights instead of actually performing actions.

The purpose of this exercise is to illustrate how Gradle works, how to work with Java libraries, and how to write more substantial Kotlin code.

Project structure

We’ll be creating a project with the following structure for our Gradle project. However, we’ll be bootstrapping this with Gradle Initializr which is a faster way to get started.

build.gradle.kts
src
|- main
| |- kotlin
| | |- com.example
| | |- App.kt
| |- resources
|- test
|- kotlin
|- resources

Starting from scratch

Follow these steps to create a new project in IntelliJ:

  1. File → New → Project
  2. Kotlin → JVM
  3. Name it kt-lights and use JDK 11+
  4. Tools → Kotlin → Configure Kotlin in project
  5. If you right click on src and click on New, you’ll see we still don’t have the option to create directories, only packages… Let’s configure Gradle first
  6. We’ll need to add a few dependencies manually to the project to bootstrap it, you can go to Gradle Initializr and select Kotlin Application with Gradle DSL, with Gradle 6.1+, in a zip, named kt-lights, with package com.example (or alternatively you can follow this guide, or you can simply run gradle init --dsl kotlin), I use Gradle Initializr as it provides comments explaining each file & code block.
  7. Extract the zip and place everything in the root of your project (delete your current src folder first if you added one)
  8. Open build.gradle.kts and you’ll see “Project x isn’t linked with Gradle
  9. Right click on build.gradle.kts and click on “Import Gradle Project”
  10. Now you’ll have access to the Gradle sidebar on the right to perform Gradle tasks such as building and running the project, running tests, etc.
  11. Let’s build it: Cmd+F9, or Build → Build Project (hammer icon)
  12. The build might take a little while, so sit back and relax :)
  13. Once it has been built you’ll see the kt-lights option in the Gradle sidebar now has an arrow with Tasks like build, application, etc.
  14. Click on Tasks → application → run in the Gradle sidebar
  15. You should see > Task :run and Hello world. at the bottom, success!

What have you done?

Let’s pause for a moment here and analyse what has happened, as a lot took place under the hood.

Your root folder probably looks something like this right now:

.gradle <- Keeps state and caches for the Gradle builds
.idea <- Created by IntelliJ, has metadata for your project/setup
build <- Contains the compiled output (you'll see .classes in here)
gradle <- Gradle lib, allows you to run it directly (not globally)
src <- Contains a folder hierarchy following Gradle's standard
build.gradle.kts <- Build script containing build/task instructions
gradlew <- Gradle wrapper script, structured info on how to run Java
gradlew.bat <- Windows version of gradlew (can be removed on *nix)
settings.gradle.kts <- Defines sub-modules for multi-module projects

At this point, consider creating a .gitignore to exclude build/out/.gradle, etc.

Open build.gradle.kts, this is your main build script, it will contain dependencies, tasks, etc. The default version from Gradle Initializr includes comments for every line, I’d suggest you start by reading these to get a feeling for the function of this file and how to write Gradle files with the Kotlin DSL.

We’ll explore how to craft a Gradle build file a bit more in depth further on.

What does gradlew do?

gradlew is a wrapper around Gradle, targeting a specific version. It will download Gradle behind the scenes if you don’t have it already. Essentially it allows you to use Gradle without downloading it with your package manager.

If you open the file you’ll notice what it does essentially boils down to this:

CLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar
JAVACMD="java"
set -- "$args0" "$args1" "$args2" "$args3" "$args4" ;; ...
eval set -- $DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS "\"-Dorg.gradle.appname=$APP_BASE_NAME\"" -classpath "\"$CLASSPATH\"" org.gradle.wrapper.GradleWrapperMain "$APP_ARGS"exec "$JAVACMD" "$@"

You can build and run your application using gradlew directly from terminal and execute the same tasks you can do in the Gradle toolbar in IntelliJ:

Click “Terminal” at the bottom of IntelliJ and enter ./gradlew run. Likewise you can use ./gradlew build to build you application (e.g. when you add dependencies, etc.), if you only perform Kotlin changes, running gradlew build might not be necessary as it will rebuild when necessary.

Crafting a Gradle build file

Groovy vs Kotlin DSL

Let’s start with a few definitions first…

Choosing a build tool:

Should you use Gradle or Maven? This article explains some of the differences. TLDR: Gradle is faster and more modern.

Writing Gradle build scripts (assuming you opt for Gradle):

  • Groovy is an object-oriented language based on Java which compiles down to Java Bytecode and runs on the JVM.
  • Gradle Groovy DSL allows you to write Gradle build scripts using Groovy.
  • Kotlin is a cross-platform, statically typed programming language with type inference which also compiles down to Java Bytecode.
  • Gradle Kotlin DSL allows you to write Gradle build scripts using Kotlin with better IDE support and tooling than what is provided by Gradle Groovy DSL.

Gradle dependencies come from… Maven?

Maven is not the same as the Maven repository manager. The Maven repository manager is an API that is implemented in a lot of services, and multiple build systems (like Gradle) use it to host artifacts, or as a source to download dependencies from. It is not only for Java artifacts (although that is its biggest use-case), you can host any artifact as long as you provide a groupId, artifactId and a version. Gitlab, for example, also hosts a Maven repository for all projects, which you can use to publish internal/private libs.

So yes, Gradle re-uses the Maven repository manager as every open source library uses that standard.

When publishing Gradle libraries you can choose Ivy or Maven repositories (e.g. jcenter & mavenCentral).

Should you use Gradle or Maven? Kotlin DSL or Groovy?

Regardless of which you choose all what is happening behind the scenes is executables run which fetch dependencies, build your project, execute a series of tasks, and start your application. Everything else is just a matter of conventions… and speed.

If you feel like reading a bit of history, this article covers how (and why) we’ve gone from Make to Ant (with Ivy), to Maven, to Gradle.

How do I identify which build system I’m using?

  • If you have a build.gradle file, the project uses Gradle with Groovy DSL
  • If you have a build.gradle.kts file, it uses Gradle with Kotlin DSL
  • If you have a pom.xml file, you’re using Maven

How does Gradle Kotlin DSL work?

Gradle ships with its own version of Kotlin, and implements features that are required for you to write your build scripts.

You’ll notice the build.gradle.kts file has a .kts extension, this means it is a Kotlin Script file, so you can write standard Kotlin in this file. It implements a few functions out-of-the-box, such as “plugins”, “dependencies”, and others… These function blocks are where we’ll place our tests configuration, dependencies, build & execution tasks, etc.

Let’s look at the plugins block in build.gradle.kts:

plugins {
id("org.jetbrains.kotlin.jvm") version "1.3.61"
id("org.jetbrains.kotlin.plugin.serialization") version "1.3.61"
application
}

This is valid Kotlin.

You can cmd+click (or ctrl+click) on plugins and it will take you to org/gradle/kotlin/dsl/KotlinBuildScript.kt.

This is a higher-order function (a function which can take functions as arguments). It takes in a lambda function called “block” as an argument. Specifically, it takes in a function literal with receiver, so unfortunately we’ve met a conundrum:

To explain this in detail we must dive into the “deep end” of Kotlin, and touch a bit of functional programming in order to understand how Gradle build files with Kotlin DSL work.

So we’ll gloss over it for now, and return to it further on, once we have gone through the basics of the language and we’re ready to tackle it.

For now the following explanation should suffice:

The plugins code block is a call to a function called “plugins”. In Kotlin and Java you usually call functions with (), however, if the last argument (in this case the only one) receives a function, you can skip the parenthesis and simply provide the function body wrapped in {}.

This could be an equally-valid way to specify your plugins block:

val pluginsBlock: PluginDependenciesSpecScope.() -> Unit = {
id("org.jetbrains.kotlin.jvm") version "1.3.61"
id("org.jetbrains.kotlin.plugin.serialization") version "1.3.61"
application
}
plugins(pluginsBlock)

This might still look a bit complicated, so we’ll cover it a bit more in detail later on when we’ve covered the basics. Let’s forget about lambdas and function literals with receivers for now and let’s renew our focus on constructing Gradle build files.

What are plugins?

Plugins can extend the Gradle DSL, add tasks, apply configuration, perform compiling tasks (Java/Kotlin), encapsulate imperative logic, amongst other things. This is what the plugins block might look like:

plugins {
java
idea
application
kotlin
("jvm") version "1.3.61"
id("org.jetbrains.kotlin.plugin.allopen") version "1.3.61"
}

In the example above, java is a core plugin (thus it can use the short notation instead of the fully qualified name like the one used for the “allopen” plugin which is a custom or community plugin).

The JavaPlugin allows us to compile Java code into JARs inside our project. The ApplicationPlugin allows us to run our application, e.g. using gradlew run. You can find a list of the other core plugins here.

By using the Plugins DSL (the plugins block) you are both resolving and applying the plugin. You can find more information on Gradle Plugins and the Plugins DSL in the official Gradle documentation.

Dependencies vs buildscript

So, you declare your dependencies like this:

dependencies {
implementation(platform("org.jetbrains.kotlin:kotlin-bom"))
implementation("org.jetbrains.kotlin:kotlin-stdlib-jdk8")
testImplementation("org.jetbrains.kotlin:kotlin-test")
}

But, you can also have a dependencies block inside the build script:

The difference is that the root-level dependencies block specifies dependencies required by the application, whereas the buildscript block can have a dependencies block which specifies dependencies that will be added to the classpath required by the build itself.

In other words, plugins and extensions/libraries which perform build-specific functions will need to be in the buildscript dependencies as opposed to the application ones.

This article dives into the buildscript block in a bit more depth.

Implementation vs testImplementation vs compile

Inside the dependencies block you can specify repositories (where to fetch dependencies from) as well as local and remote modules.

Gradle allows producers to declare api and implementation dependencies to prevent unwanted libraries from leaking into the classpaths of consumers.

If you find any compile dependencies, these can be replaced with implementation, as compile is obsolete.

You’ll notice some dependencies marked as implementation, testImplementation or debugImplementation, as well as their “api” variants, the answers on this StackOverflow question (as well as the official docs) provide insight into the differences between each.

The name of a package/dependency consists of a “group”, the package “name” and the version.

Examples of when to use each:

  • I am writing a Hue library and want to expose the Java Hue API to consumers of my library, I will use api instead of implementation for the Hue library I’ll be importing
  • I am writing a web API in Kotlin which will also call other services using Fuel, so fuel, and kotlin-stdlib-jdk8 will be implementation dependencies.
  • I am writing unit tests and I require several libraries only when compiling and running the test sources, I will use testImplementation for kotlin-test and kotlin-test-junit

Writing your own Gradle tasks:

Run ./gradle tasks to see a list of tasks you can execute.

Tasks like “run”, “build” and “clean” are available by default after importing the application plugin and running gradle build. However, you can also write your own tasks (e.g. to auto-generate code, run specific tests, lint, etc.)

For example, you can add the following two tasks to build.gradle.kts:

var applicationName = "Hue"tasks.register("one") {
doLast {
applicationName = "HueLights"
println("The name is $applicationName")
}
}
tasks.register("two") {
dependsOn("one")
doLast {
applicationName = "Hue Lights"
println("Nope, the name is $applicationName")
}
}

If you run ./gradlew two in the terminal you’ll have this output:

> Task :one
The name is HueLights
> Task :two
Nope, the name is Hue Lights

The reason is that running “two” invokes task “one” as it depends on it.

You’ll notice these two new tasks will appear under “other” tasks in the Gradle toolbar. Why is this? What if we wanted to customise where our tasks appear?

If you look at your plugins block, you’ll notice you are using the application plugin. Now scroll down to the application block, you’ll see mainClassName defined there, but you can customise more things in this block!

The application plugin is what actually gives us the “run” Gradle task.

My first thought was to put my two task.register commands inside the application block, but this didn’t automatically move them into the “application” group in the sidebar.

Change the code we entered previously to this:

var applicationName = "Hue"tasks.register("one") {
group = "documentation"
description = "Task one description"

doLast {
applicationName = "HueLights"
println("The name is $applicationName")
}
}
tasks.register("two") {
group = "documentation"
description = "Task two description"

dependsOn("one")
doLast {
applicationName = "Hue Lights"
println("Nope, the name is $applicationName")
}
}

After rebuilding (which should happen automatically) you’ll see both tasks appear under “documentation” now. This is because we set the “group” property, and we added a description as well.

Feel free to remove these tasks if you added them to the project at this point as we won’t be needing them.

While researching this I found a some websites use different ways of defining tasks, for example:

task<Test>("integrationTest") {
...
}
tasks.register<JacocoCoverageVerification>("coverage") {
...
}

If you cmd+click on the first task keyword you’ll see that what is actually happening behind the scenes is this:

So task is just shorthand for tasks.create! But, what is the difference between create and register?

The documentation for create states it creates a Task with the given name and type, configures it with the given action, and adds it to the container.

The documentation for register states it defines a new task, which will be created and configured when it is required. A task is ‘required’ when the task is located using query methods such as getByName(string), when the task is added to the task graph for execution or when Provider#get() is called on the return value of this method.

In other words, register is preferred, as it will only create it if needed for the build. Register only became available as of Gradle 5.1

Let’s add a Hue library to our project:

We’ll be adding this one, primarily because it is on Maven and I think it might be a good time to see how to add Maven dependencies to a Gradle project.

If you look at the bottom of the README you’ll see a maven section with groupId, artifactId and version. We’ll add them like this (in order) in our dependencies section in build.gradle.kts:

implementation("io.github.zeroone3010:yetanotherhueapi:1.2.0")

Now try running Tasks → Build → Build in the Gradle sidebar in IntelliJ (or ./gradlew build)

It should succeed.

You might see this pop up on the bottom-right, click on Enable Auto-Import:

In our scenario it seems like it was able to automatically fetch the Hue library even though we didn’t explicitly set maven as a repository, but for other libraries this might not work and you’ll explicitly have to add the repositories. This is an example of a repositories section in build.gradle.kts:

import java.net.URI // Must be at the top of build.gradle.ktsrepositories {
mavenCentral()
jcenter()
maven {
url = URI("https://jcenter.bintray.com")
}
maven {
url = URI("https://oss.sonatype.org/content/repositories/snapshots/")
}
maven {
url = URI("https://dl.bintray.com/micronaut/core-releases-local")
}
}

Let’s add the Serialization plugin

Kotlin Serialization is a compiler plugin that will allow us to serialise objects to multiple formats to keep state, save them to the filesystem or a DB, etc. For example, you might have an object with a few properties that you want to convert to JSON, this plugin simplifies that process.

There are multiple ways of serialising/deserialising in Java/Kotlin, some are libraries like Jackson or Gson, some frameworks like Micronaut provide wrappers for serialisation, but we’ll be using the Kotlin Serialization compiler plugin to illustrate how compiler plugins work.

What are compiler plugins?

As briefly mentioned previously, Kotlin compiler plugins (like “All-open”, “No-arg”, etc.) run at compile time and can perform actions like code generation, code changes, extend the language with more functionality/extensions, analyse annotations and inject code, etc.

This podcast dives more into compiler plugins, as documentation (in March 2020) is still sparse.

The podcast provides this example: Things that can’t be solved by annotations (or would lead to too much complexity) could be solved by compiler plugins, for example: JVM builders for data classes. If you have a data class with a lot of parameters, the construction might be complex, with a lot of parameters. A compiler plugin could help with the boilerplate (and add support for the auto-generated methods in the IDE).

This is the official repo for Kotlin Serialization. As of March 2020 it is not the best way to serialise/deserialise performance-wise, there are also differences between the several ways of doing this, for example Kotlin Serialization supports mapping for default values in data classes, Micronaut on the other hand facilitates validation when serialising/deserialising. When choosing a serialiser for your project, research the different options and choose the right one for you.

Let’s add Kotlin Serialization to our project…

Go to build.gradle.kts and add this section above the dependencies section:

buildscript {
repositories {
mavenCentral()
}
dependencies {
classpath("org.jetbrains.kotlin:kotlin-gradle-plugin:1.3.61")
classpath("org.jetbrains.kotlin:kotlin-serialization:1.3.61")
}
}

As Kotlin Serialization is a compiler plugin, we added those in a buildscript block, as we only need them in the classpath during build time.

Now add this inside the dependencies section:

// Serialization
implementation("org.jetbrains.kotlinx:kotlinx-serialization-runtime:0.14.0")

Finally in the plugins section apply the plugin:

// Serialization
id("org.jetbrains.kotlin.plugin.serialization") version "1.3.61"

This is the actual implementation of it, meaning it will execute this during build time. This will extend classes marked with the @Serializable annotation with methods allowing serialisation/deserialisation.

Why are we adding serialisation to our project?

In the real world you might add it to convert a data class to JSON and POST it to a web API. In our case we might want to capture the state of our lights and store it (serialised) in a file before closing the program, and loading the state from a file (deserialising) when starting the program.

While we’re here, let’s specify the target JVM bytecode version

Add import org.jetbrains.kotlin.gradle.tasks.KotlinCompile at the top

Add the following code block anywhere at the top level (could be below dependencies for example):

tasks.withType<KotlinCompile> {
kotlinOptions.jvmTarget = "1.8"
}

This specifies which version of Java to compile the bytecode for, read this for more information. This will prevent issues where a library compiled for 1.8 (e.g. Kodein) is incompatible with your code (where 1.6 is the default).

More Gradle resources

Gradle is super extensive, I’ve only scratched the surface of what we can do , and I’ve already rambled on for far too long! There are entire books just dedicated to Gradle. As I don’t want to focus solely on Gradle for this whole tutorial, let’s move on…

If you’d like to dive further into Gradle/tasks I suggest the following links:

Thanks for reading, click here to go to the next part.

--

--

Mike Warner

Software Engineer specialised in PHP, Python, NodeJS, Kotlin and React