Building Kotlin Applications with libraries Sample
You can open this sample in an IDE that supports Gradle. |
To prepare your software project for growth, you can organize a Gradle project into multiple subprojects to modularize the software you are building. In this guide, you’ll learn how to structure such a project on the example of a Kotlin application. However, the general concepts apply for any software you are building with Gradle. You can follow the guide step-by-step to create a new project from scratch or download the complete sample project using the links above.
What you’ll build
You’ll build a Kotlin application that consists of an application and multiple library projects.
What you’ll need
-
A text editor or IDE - for example IntelliJ IDEA
-
A Java Development Kit (JDK), version 8 or higher - for example AdoptOpenJDK
-
The latest Gradle distribution
Create a project folder
Gradle comes with a built-in task, called init
, that initializes a new Gradle project in an empty folder.
The init
task uses the (also built-in) wrapper
task to create a Gradle wrapper script, gradlew
.
The first step is to create a folder for the new project and change directory into it.
$ mkdir demo $ cd demo
Run the init task
From inside the new project directory, run the init
task using the following command in a terminal: gradle init
.
When prompted, select the 1: application
project type and 2: Kotlin
as the implementation language.
Afterwards, select 2: Application and library project
.
Next you can choose the DSL for writing buildscripts - 1 : Kotlin
or 2: Groovy
.
For the other questions, press enter to use the default values.
The output will look like this:
$ gradle init Select type of build to generate: 1: Application 2: Library 3: Gradle plugin 4: Basic (build structure only) Enter selection (default: Application) [1..4] 1 Select implementation language: 1: Java 2: Kotlin 3: Groovy 4: Scala 5: C++ 6: Swift Enter selection (default: Java) [1..6] 2 Project name (default: demo): Enter target Java version (min: 7, default: 21): Select application structure: 1: Single application project 2: Application and library project Enter selection (default: Single application project) [1..2] 2 Select build script DSL: 1: Kotlin 2: Groovy Enter selection (default: Kotlin) [1..2] Select test framework: 1: JUnit 4 2: TestNG 3: Spock 4: JUnit Jupiter Enter selection (default: JUnit Jupiter) [1..4] Generate build using new APIs and behavior (some features may change in the next minor release)? (default: no) [yes, no] BUILD SUCCESSFUL 1 actionable task: 1 executed
The init
task generates the new project with the following structure:
├── gradle (1)
│ ├── libs.versions.toml (2)
│ └── wrapper
│ ├── gradle-wrapper.jar
│ └── gradle-wrapper.properties
├── gradlew (3)
├── gradlew.bat (3)
├── settings.gradle.kts (4)
├── buildSrc
│ ├── build.gradle.kts (5)
│ ├── settings.gradle.kts (5)
│ └── src
│ └── main
│ └── kotlin (6)
│ ├── buildlogic.kotlin-application-conventions.gradle.kts
│ ├── buildlogic.kotlin-common-conventions.gradle.kts
│ └── buildlogic.kotlin-library-conventions.gradle.kts
├── app
│ ├── build.gradle.kts (7)
│ └── src
│ ├── main (8)
│ │ └── java
│ │ └── demo
│ │ └── app
│ │ ├── App.java
│ │ └── MessageUtils.kt
│ └── test (9)
│ └── java
│ └── demo
│ └── app
│ └── MessageUtilsTest.kt
├── list
│ ├── build.gradle.kts (7)
│ └── src
│ ├── main (8)
│ │ └── java
│ │ └── demo
│ │ └── list
│ │ └── LinkedList.kt
│ └── test (9)
│ └── java
│ └── demo
│ └── list
│ └── LinkedListTest.kt
└── utilities
├── build.gradle.kts (7)
└── src
└── main (8)
└── java
└── demo
└── utilities
├── JoinUtils.kt
├── SplitUtils.kt
└── StringUtils.kt
├── gradle (1)
│ ├── libs.versions.toml (2)
│ └── wrapper
│ ├── gradle-wrapper.jar
│ └── gradle-wrapper.properties
├── gradlew (3)
├── gradlew.bat (3)
├── settings.gradle (4)
├── buildSrc
│ ├── build.gradle (5)
│ ├── settings.gradle (5)
│ └── src
│ └── main
│ └── groovy (6)
│ ├── buildlogic.kotlin-application-conventions.gradle
│ ├── buildlogic.kotlin-common-conventions.gradle
│ └── buildlogic.kotlin-library-conventions.gradle
├── app
│ ├── build.gradle (7)
│ └── src
│ ├── main (8)
│ │ └── java
│ │ └── demo
│ │ └── app
│ │ ├── App.java
│ │ └── MessageUtils.java
│ └── test (9)
│ └── java
│ └── demo
│ └── app
│ └── MessageUtilsTest.java
├── list
│ ├── build.gradle (7)
│ └── src
│ ├── main (8)
│ │ └── java
│ │ └── demo
│ │ └── list
│ │ └── LinkedList.java
│ └── test (9)
│ └── java
│ └── demo
│ └── list
│ └── LinkedListTest.java
└── utilities
├── build.gradle (7)
└── src
└── main (8)
└── java
└── demo
└── utilities
├── JoinUtils.java
├── SplitUtils.java
└── StringUtils.java
1 | Generated folder for wrapper files |
2 | Generated version catalog |
3 | Gradle wrapper start scripts |
4 | Settings file to define build name and subprojects |
5 | Build script of buildSrc to configure dependencies of the build logic |
6 | Source folder for convention plugins written in Groovy or Kotlin DSL |
7 | Build script of the three subprojects - app , list and utilities |
8 | Kotlin source folders in each of the subprojects |
9 | Kotlin test source folders in the subprojects |
You now have the project setup to build a Kotlin application which is modularized into multiple subprojects.
Review the project files
The settings.gradle(.kts)
file has two interesting lines:
rootProject.name = "demo"
include("app", "list", "utilities")
rootProject.name = 'demo'
include('app', 'list', 'utilities')
-
rootProject.name
assigns a name to the build, which overrides the default behavior of naming the build after the directory it’s in. It’s recommended to set a fixed name as the folder might change if the project is shared - e.g. as root of a Git repository. -
include("app", "list", "utilities")
defines that the build consists of three subprojects in the corresponding folders. More subprojects can be added by extending the list or adding moreinclude(…)
statements.
Since our build consists of multiple-subprojects, we want to share build logic and configuration between them.
For this, we utilize so-called convention plugins that are located in the buildSrc
folder.
Convention plugins in buildSrc
are an easy way to utilise Gradle’s plugin system to write reusable bits of build configuration.
in this sample, we can find three such convention plugins that are based on each other:
plugins {
id("org.jetbrains.kotlin.jvm") (1)
}
repositories {
mavenCentral() (2)
}
dependencies {
constraints {
implementation("org.apache.commons:commons-text:1.12.0") (3)
}
testImplementation("org.junit.jupiter:junit-jupiter:5.11.1") (4)
testRuntimeOnly("org.junit.platform:junit-platform-launcher")
}
tasks.named<Test>("test") {
useJUnitPlatform() (5)
}
plugins {
id 'org.jetbrains.kotlin.jvm' (1)
}
repositories {
mavenCentral() (2)
}
dependencies {
constraints {
implementation 'org.apache.commons:commons-text:1.12.0' (3)
}
testImplementation 'org.junit.jupiter:junit-jupiter:5.11.1' (4)
testRuntimeOnly 'org.junit.platform:junit-platform-launcher'
}
tasks.named('test') {
useJUnitPlatform() (5)
}
The kotlin-common-conventions
defines some configuration that should be shared by all our Kotlin project — independent of whether they represent a library or the actual application.
First, we apply the Kotlin Gradle plugin (1) to have all functionality for building Kotlin projects available.
Then, we declare a repository — mavenCentral()
— as source for external dependencies (2), define dependency constraints (3) as well as standard dependencies that are shared by all subprojects and set JUnit 5 as testing framework (4…).
Other shared settings, like compiler flags or JVM version compatibilities, could also be set here.
plugins {
id("buildlogic.kotlin-common-conventions") (1)
`java-library` (2)
}
plugins {
id 'buildlogic.kotlin-common-conventions' (1)
id 'java-library' (2)
}
plugins {
id("buildlogic.kotlin-common-conventions") (1)
application (2)
}
plugins {
id 'buildlogic.kotlin-common-conventions' (1)
id 'application' (2)
}
Both kotlin-library-conventions
and kotlin-application-conventions
apply the kotlin-common-conventions
plugin (1) so that the configuration performed there is shared by library and application projects alike.
Next they apply the java-library
or application
plugin respectively (2) thus combining our common configuration logic with specifics for a library or application.
While there is no more fine grained configuration in this example, library or application project specific build configuration can go into one of these convention plugin scripts.
Lets have a look at the build.gradle(.kts)
files in the subprojects.
plugins {
id("buildlogic.kotlin-application-conventions")
}
dependencies {
implementation("org.apache.commons:commons-text")
implementation(project(":utilities"))
}
application {
mainClass = "demo.app.AppKt" (1)
}
plugins {
id 'buildlogic.kotlin-application-conventions'
}
dependencies {
implementation 'org.apache.commons:commons-text'
implementation project(':utilities')
}
application {
mainClass = 'demo.app.AppKt' (1)
}
plugins {
id("buildlogic.kotlin-library-conventions")
}
plugins {
id 'buildlogic.kotlin-library-conventions'
}
plugins {
id("buildlogic.kotlin-library-conventions")
}
dependencies {
api(project(":list"))
}
plugins {
id 'buildlogic.kotlin-library-conventions'
}
dependencies {
api project(':list')
}
Looking at the build scripts, we can see that they include up to three blocks
-
Every build script should have a
plugins {}
block to apply plugins. In a well-structured build, it may only apply one convention plugin as in this example. The convention plugin will then take care of applying and configuring core Gradle plugins (likeapplication
orjava-library
) other convention plugin or community plugins from the Plugin Portal. -
Second, if the project has dependencies, a
dependencies {}
block should be added. Dependencies can be external, such as the JUnit dependencies we add inkotlin-common-conventions
, or can point to other local subprojects. For this, theproject(…)
notation is used. In our example, theutilities
library requires thelist
library. And theapp
makes use of theutilities
library. If local projects depend on each other, Gradle takes care of building dependent projects if (and only if) needed. To learn more, have a look at the documentation about dependency management in Gradle. -
Third, there may be one or multiple configuration blocks for plugins. These should only be used in build scripts directly if they configure something specific for the one project. Otherwise, such configurations also belong into a convention plugin. In this example, we use the
application {}
block, which is specific to theapplication
plugin, to set themainClass
in ourapp
project todemo.app.App
(1).
The last build file we have is the build.gradle(.kts)
file in buildSrc
.
plugins {
`kotlin-dsl` (1)
}
repositories {
gradlePluginPortal() (2)
}
dependencies {
implementation(libs.kotlin.gradle.plugin)
}
plugins {
id 'groovy-gradle-plugin' (1)
}
repositories {
gradlePluginPortal() (2)
}
dependencies {
implementation libs.kotlin.gradle.plugin
}
This file is setting the stage to build the convention plugins themselves.
By applying one of the plugins for plugin development — groovy-gradle-plugin
or kotlin-dsl
— (1) we enable the support for writing convention plugins as build files in buildSrc
.
Which are the convention plugins we already inspected above.
Furthermore, we add Gradle’s plugin portal as repository (2), which gives us access to community plugins.
To use a plugin it needs to be declared as dependency in the dependencies {}
block.
Apart from the Gradle build files, you can find example Kotlin source code and test source code in the corresponding folders. Feel free to modify these generated sources and tests to explore how Gradle reacts to changes when running the build as described next.
Run the tests
You can use ./gradlew check
to execute all tests in all subprojects.
When you call Gradle with a plain task name like check
, the task will be executed for all subprojects that provide it.
To target only a specific subproject, you can use the full path to the task.
For example :app:check
will only execute the tests of the app
project.
However, the other subprojects will still be compiled in the case of this example, because app
declares dependencies to them.
$ ./gradlew check BUILD SUCCESSFUL 9 actionable tasks: 9 executed
Gradle won’t print more output to the console if all tests passed successfully.
You can find the test reports in the <subproject>/build/reports
folders.
Feel free to change some of the example code or tests and rerun check
to see what happens if a test fails.
Run the application
Thanks to the application
plugin, you can run the application directly from the command line.
The run
task tells Gradle to execute the main
method in the class assigned to the mainClass
property.
$ ./gradlew run > Task :app:run Hello world! BUILD SUCCESSFUL 2 actionable tasks: 2 executed
The first time you run the wrapper script, gradlew , there may be a delay while that version of gradle is downloaded and stored locally in your ~/.gradle/wrapper/dists folder.
|
Bundle the application
The application
plugin also bundles the application, with all its dependencies, for you.
The archive will also contain a script to start the application with a single command.
$ ./gradlew build BUILD SUCCESSFUL in 0s 8 actionable tasks: 8 executed
If you run a full build as shown above, Gradle will have produced the archive in two formats for you:
app/build/distributions/app.tar
and app/build/distributions/app.zip
.
Publish a Build Scan
The best way to learn more about what your build is doing behind the scenes, is to publish a build scan.
To do so, just run Gradle with the --scan
flag.
$ ./gradlew build --scan BUILD SUCCESSFUL in 0s 8 actionable tasks: 8 executed Publishing a build scan to scans.gradle.com requires accepting the Gradle Terms of Service defined at https://gradle.com/terms-of-service. Do you accept these terms? [yes, no] yes Gradle Terms of Service accepted. Publishing build scan... https://gradle.com/s/5u4w3gxeurtd2
Click the link and explore which tasks where executed, which dependencies where downloaded and many more details!
Summary
That’s it! You’ve now successfully configured and built a Kotlin application project with Gradle. You’ve learned how to:
-
Initialize a project that produces a Kotlin application
-
Create a modular software project by combining multiple subprojects
-
Share build configuration logic between subprojects using convention plugins in
buildSrc
-
Run similar named tasks in all subprojects
-
Run a task in a specific subproject
-
Build, bundle and run the application
Next steps
When your project grows, you might be interested in more details how to configure JVM projects, structuring multi-project builds and dependency management: