#
Building with Gradle
When developing applications, there is a large list of steps that need to be completed before we can release our software to users. We might need to:
- Make sure that we have the correct version of source code and assets.
- Check that we have the correct versions of external libraries.
- Make sure we're running on the correct version of the OS.
- Setup our build environment and compile everything.
- Setup our test environment, and run tests.
- Build installers to distribute to users.
Performing these steps manually is error-prone and time-consuming. Instead of doing this by-hand, we tend to rely on build systems: software that is used to build other software. There are a number of build systems on the market that attempt to simplify and propvide consistency in these tasks. These tools often language or toolchain dependent due to the specifics of each programming language. e.g. make or scons for C++; cargo for Rust, maven for Java.
Builds systems automate what would otherwise be a manual process. They provide consistency and help address issues like:
- How do I make sure that all of the steps are being handled properly?
- How do I ensure that everyone is building software the same way i.e. using the same compiler options, and libraries?
- How do I ensure that tests are being run before changes are committed?
You learned to use make
in previous courses. Make
is one of the most widely used build systems and can be used to build everything from small projects to medium systems. However, it has some limitations, especially with larger projects.
- Build dependencies must be explicitly defined. Libraries must be present on the build machine, and manually maintained i.e. someone needs to download, test and install them.
Make
is tied to the underlying environment of the build machine. It relies on shell scripts and environment variables to work, and uses locally installed software to build. This makes your build environment fragile and easy to break.- Performance is poor.
Make
does not scale well to large projects. - It is challenging to fully automate and integrate with other systems.
#
Requirements
We’re going to use Gradle in this course:
- It handles our long-list of requirements (which is, frankly, pretty impressive).
- It fits nicely into the Kotlin and JVM ecosystem, and is recommended for new projects.
- It’s the official build tool for Android builds, so you will need it for Android applications.
- It's cross-platform and language agnostic.
- It's open source and has a large community of users.
Let's introduce some of the key features of Gradle. These are features that most modern build systems will provide; we'll discuss them in terms of how they are implemented in Gradle specifically.
#
Build Tasks
Projects often have complex build requirements that include a series of steps that need to be performed. Minimally, you need to:
- compile your source code,
- run tests to make sure it built and works properly,
- build a distributable package.
You might also have additional steps: generate documentation, run static analysis, or deploy to a server.
Any build system needs to support a wide range of tasks like this, and it should allow you to define how these tasks will be performed. It should also run them in the correct order.
Gradle uses the term task
to describe a single unit of work that needs to be performed. You can define tasks to compile code, run tests, build a distributable package, and so on. You can also define dependencies between tasks, so that one task runs before another. This makes it easy to build complex projects, and ensures that the build process is consistent across all of your projects.
Luckily, Gradle knows how to do most of these tasks out-of-the-box, so you typically just need to provide configuration details. For example, you can run the build
task to compile your code, run tests, and build a distributable package. We'll discuss tasks in more detail in the next sections.
#
Build Configuration
It's certainly possible to write custom scripts e.g, bash shell scripts to execute these tasks, but they are challenging to maintain, and need to be written for each project. Gradle provides a way to define tasks in build configuration files, and then run them with a single command. This makes it easy to build complex projects, and ensures that the build process is consistent across all of your projects.
Unlike other configuration-based build systems, Gradle uses a Domain Specific Language (DSL) to define build scripts; you actually write your scripts in Groovy or Kotlin. This makes Gradle extremely configurable and extensible to meet complex project requirements.
We'll discuss build configuration in more detail in the next sections as well.
#
Dependency Management
The final core function is managing libraries and dependencies.
When we write software, we often rely on external libraries to provide functionality that we don't want to write ourselves. For example, we might use some library to handle networking, or to provide a user interface. These libraries are known as dependencies
, and they are a critical part of modern software development.
A large challenge of any build system is managing these dependencies properly. For example, you need to make sure that you have the correct version of a library, and that any dependencies that it might need are also installed (called transitive dependencies). You also need to make sure that the library is compatible with the rest of your software, and that it doesn't introduce any security vulnerabilities.
Gradle is designed so that you can specify your dependencies in your build scripts. Gradle will download them from an online repository as part of your build process, and manage them for you.
mavenCentral is the standard online repository for Java and Kotlin libraries.
#
Gradle Concepts
Gradle leverages build tasks and build configuration files to provide a flexible and powerful build system.
In Gradle, a task
is a set of related functions that can be applied to a particular type of project. For example, you might have a build
task to build your Kotlin console project, and a clean
task to remove temporary build files. Complex projects will add additional tasks that are required for that style of project e.g., build my Android app
.
Gradle is declarative, in that you describe what you want to do, rather than how you want to do it. e.g., "compile my Kotlin application" rather than "run the kotlinc
compiler with a particular set of options". You describe your tasks in a build script, and Gradle takes care of running them in the correct order.
#
Project Setup
Your Gradle project is simply a directory structure with a set of configuration files that describe how to build your project i.e. that tell Gradle which plugins to apply, which dependencies to use, and which tasks to run.
#
Create a Project
Use the File
> New Project
wizard in IntelliJ to create a new empty project. This will give you a top-level project with starting configuration files.
Projects can be created with a variety of configurations, but for this course, you should choose Kotlin
as your programming language, Gradle
for your build system, and Kotlin
for your DSL language.
The result of this process is a new project with a directory structure and configuration files that are ready to use with Gradle. Note that the project will be specific to the type of project that you chose e.g., Android, Desktop, Web Service.
If you have Gradle installed on your machine, you can create a new project from the command line using the gradle init
command. This will create a new project with the default configuration files.
We tend to use the IntelliJ wizard, as it provides a more complete project structure, is easier to use, and doesn't require a separate Gradle installation.
#
Using the Gradle Wrapper
At the top-level of your project's directory structure will be a script, gradlew
(or gradlew.bat
for Windows users). This is the Gradle wrapper, and it is a script that you use to run Gradle tasks without having to install Gradle on your machine. You run it and pass it the same command-line arguments that you would normally pass to Gradle.
For example, we can invoke the Gradle wrapper with the help
task to get information about how Gradle is configured and how to use it. This is the same as running gradle help
if you had Gradle installed on your machine.
$ ./gradlew help
Starting a Gradle Daemon (subsequent builds will be faster)
> Task :help
Welcome to Gradle 7.5.1.
To run a build, run gradlew <task> ...
To see a list of available tasks, run gradlew tasks
To see more detail about a task, run gradlew help --task <task>
To see a list of command-line options, run gradlew --help
For more detail on using Gradle, see https://docs.gradle.org/7.5.1/userguide/command_line_interface.html
For troubleshooting, visit https://help.gradle.org
Aside from convenience, we use a wrapper to ensure that tasks are always executed with the correct version of Gradle. If needed, the wrapper script will download and cache the version of Gradle configured for your project, and then use it to run the tasks that you specified.
#
Wrapper Configuration
The Gradle project configuration (gradle/gradle-wrapper.properties
) lists the version of Gradle to be used for your project.
To change the version of Gradle being used, update the gradle-wrapper.properties
file, and change the distributionURL
line to the correct version e.g., Gradle 8.0.2 below.
distributionBase=GRADLE_USER_HOME
distributionPath=wrapper/dists
distributionUrl=https\://services.gradle.org/distributions/gradle-8.0.2-bin.zip
zipStoreBase=GRADLE_USER_HOME
zipStorePath=wrapper/dists
The gradle-wrapper.properties
file should be stored in Git, as part of your build configuration scripts.
You should never run the Gradle executable directly, but always run Gradle tasks through the Gradle wrapper. The ensures that you are running the correct version of Gradle for your project.
#
Build Tasks
In Gradle, a task
is a set of related functions that can be applied to a particular type of project. Some Gradle tasks are built-in, but others will need to be specified. Let's discuss working with tasks first.
#
Running Tasks
To run Gradle tasks from the command line, use the Gradle wrapper with the appropriate the task name. We can run ./gradlew tasks
to see a list of tasks that are supported in your project.
$ ./gradlew tasks
> Task :tasks
------------------------------------------------------------
Tasks runnable from root project 'gradle'
------------------------------------------------------------
Application tasks
-----------------
run - Runs this project as a JVM application
Build tasks
-----------
assemble - Assembles the outputs of this project.
build - Assembles and tests this project.
buildDependents - Assembles and tests this project and all projects that depend on it.
buildNeeded - Assembles and tests this project and all projects it depends on.
classes - Assembles main classes.
clean - Deletes the build directory.
jar - Assembles a jar archive containing the classes of the 'main' feature.
testClasses - Assembles test classes.
# many more that are not shown
You can execute any of these tasks through the Gradle wrapper. e.g. ./gradlew build
or ./gradlew clean
.
Commonly used tasks include:
./gradlew help
will provide online help../gradlew tasks
will list all of the tasks that are available../gradlew clean
will remove temporary build files../gradlew build
will build your project../gradlew run
will usually run your project (depending on the platform).
You can also run tasks from within IntelliJ. Both IntelliJ IDEA and Android Studio include a Gradle IDE plugin, which allows you to run Gradle tasks.
View
> Tool Windows
> Gradle
will open the Gradle window. Tasks are grouped by category. You can run individual tasks by double-clicking on them.
#
Using Plugins
Gradle comes with a small number of predefined tasks. You will usually need to add additional tasks that are specific to your type of project. We do this via plugins
.
A plugin
is a collection of tasks that have been bundled together to perform a specific function. For example, the java
plugin adds tasks for compiling Java code, running tests, and building a distributable package. Plugins can also add additional configuration options, or set defaults for existing options.
There are different types of plugins.
- Core plugins: These are the plugins that are included with Gradle by default. They provide basic functionality that is required by many projects. Core plugins include
java
(which adds support for compiling Java code), andapplication
(which adds support for running code, and building a distributable package).
Core plugins are listed in the Gradle documentation, and you can use them in your project by adding a line to the plugins
section of your build.gradle.kts
file.
plugins {
java
}
- Community Plugins: These are plugins that are created by the community and are not included with Gradle by default. They provide additional functionality that is not available in the core plugins. An example would be the 'io.github.gmazzo.codeowners.jvm' plugin, which provides some very specific functionality.
Community plugins can be found in the Gradle Plugin Portal. This is a repository of plugins that have been created by the community. You can search for plugins by name, or by category.
Under the plugin page on the Gradle Plugin Portal, you will find instructions on how to add the plugin to your project. Typically, you will need to add a line to the plugins
section of your build.gradle.kts
file.
Running ./gradlew tasks
will list all of the tasks that are available in your project. This will include tasks that are added by plugins.
#
Build Configuration
This directory structure is opinionated: Gradle requires a specific directory structure to work correctly, so you should work within the structure that it gives you. Most of these files are configuration file, or scripts that help execute gradle. The actual source code will be placed in the app/src
directory.
A standard Gradle project has the following directory structure:
.
├── build.gradle.kts
├── gradle
│ └── wrapper
├── gradle.properties
├── gradlew
├── gradlew.bat
├── settings.gradle.kts
└── src
├── main
└── test
You can choose Android to build an Android project as well. It will produce an identical directory structure; only the configuration files will be different.
#
Configuration Files
build.gradle.kts
and settings.kts
are the configuration files for your project, describing logical structure, dependencies, and so on.
We strongly recommend that you use the New Project wizard to create your project. This will ensure that you have the correct directory structure and configuration files.
settings.gradle.kts
This is the top-level configuration file. You don't need to modify this for single-target projects.
plugins {
id("org.gradle.toolchains.foojay-resolver-convention") version "0.5.0"
}
// top-level descriptive name
rootProject.name = "project-name"
build.gradle.kts
This is the detailed build configuration. You might need to modify this file to:
- Add a new dependency (i.e. library)
- Add a new plugin (i.e. set of custom tasks)
- Update the version number of a product release (
version
below).
// includes jvm tasks
plugins {
kotlin("jvm") version "1.9.21"
}
// product release info
group = "org.example"
version = "1.0-SNAPSHOT"
// location to find libraries
repositories {
mavenCentral()
}
// add libraries here
dependencies {
testImplementation(`org.jetbrains.kotlin:kotlin-test`)
}
tasks.test {
useJUnitPlatform()
}
// java version
kotlin {
jvmToolchain(17)
}
#
Multi-Project Builds
A simple build is suitable for most standalone, independent projects. Keep in mind that it is restricted to the specific project type that you chose with the New Project wizard e.g., Android, Desktop, Web Service.
What do you do if you want to build a more complex project e.g., a combination of desktop, Android, web service and so on? You could do this with multiple separate projects, but it's often beneficial to have them in the same project.
In Gradle, this is known as a multi-project build
, and it allows you to build multiple projects from a single top-level project. This is useful when you have multiple projects that depend on each other, or when you want to share code between projects.
There is a specific structure that you need to follow to create a multi-project build. In the example below, we have subprojects application, models and server which represent different projects.
.
├── application
│ ├── bin
│ ├── build.gradle.kts
│ └── src
├── gradle
│ └── wrapper
├── gradle.properties
├── gradlew
├── gradlew.bat
├── local.properties
├── models
│ ├── bin
│ ├── build.gradle.kts
│ └── src
├── server
│ ├── bin
│ ├── build.gradle.kts
│ └── src
└── settings.gradle.kts
To create a multi-project build, you need to create subprojects for each project that you want to manage. Each subproject should have its own build.gradle.kts
file, and a src
directory containing the source code for that project. The top-level settings.gradle.kts
describes common dependencies and the overall project structure.
Let's walk through an example of how to create a multi-project build.
#
Step 1: Move the application code into an application
project:
Move the application code into an application
project:
- Create a directory named
application
. - Move the
build.gradle.kts
andsrc
folder from the root into theapplication
directory. - Edit the top-level
settings.gradle.kts
and add the following line to the bottom of the file:
include("application")
#
Step 2: Add a server
project
To add a server
project:
- Create a type of project using the IntelliJ IDEA Project Wizard e.g., Ktor server.
- Place it in a subdirectory of your main project. Each subdirectory should just contain the generated
src
directory, and thebuild.gradle.kts
file. You don't want anything else in the project subdirectory. - Finally, update the
settings.gradle.kts
to add each project subdirectory in theinclude
statement.
include("application", "server")
#
Step 3: Add a models
project
Repeat Step 2 with an empty project to hold shared models.
#
Dependency Management
We'll use Gradle for managing our dependencies. Gradle can download specific versions of libraries for us from an online repository: a location where libraries are stored and made available. Typically, a repository will offer a large collection of libraries and include many years of releases, so that a package manager is able to request a specific version of a library and all of its dependencies.
#
Setting Up Repositories
Repositories can be local (e.g. a large company can maintain its own repository of internal or external libraries), or external (e.g. a collection of public libraries). The most popular Java/Kotlin repository is mavenCentral, and we'll use it with Gradle to import any external dependencies that we might require.
You can control the repository that Gradle uses by specifying its location in the build.gradle.kts
file. By default, Maven Central should already be included.
repositories {
mavenCentral()
}
#
Browsing Repositories
You can browse repository websites to find libraries that you might want to use. Additionally, you can use an online package directory.
Each package information page will include the details of how to import the package into your Gradle project. e.g.
Also see Resources > Libraries for links to popular libraries.
#
Add Dependencies
You add a specific module or dependency by adding it into the dependencies section of the build.gradle.kts
file. Dependencies need to be specified using a "group name: module name: version number" (with a colon separating each one).
From the previous example, we can copy and paste the dependency line from the package information page directly into our build.gradle.kts
dependencies {
implementation("io.coil-kt.coil3:coil-jvm:3.0.0-alpha06")
}
You generally want the most recent version that is available, but you can specify a specific version if you need to. Gradle will download the library and any dependencies that it needs, and make them available to your project.
The keyword implementation
above indicates that this dependency is required for the application to both compile and run. There are other keywords that can be used to specify different types of dependencies e.g.,
runtimeOnly
for dependencies that are only required at runtime.testImplementation
for dependencies that are only required for testing.api
when writing libraries, to indicate that this is a transitive dependencies need to be exported.
You should use implementation
for most dependencies.
#
Version Catalogs
One challenge is keeping track of the versions of libraries that you are using. It's important to keep your libraries up-to-date to ensure that you have the latest features and bug fixes. However, updating libraries can be a time-consuming process, as you need to check the library's website for new versions, update your build file, and test your application to make sure that everything still works.
Gradle has a feature called version catalogs
, which is a centralized file that contains a list of libraries and their versions. When you run a Gradle build, Gradle will check the version catalog to see if there are any updates available for the libraries that you are using. If there are updates available, Gradle will automatically update the libraries in your project to the latest versions.
When using multiple related projects, this can help ensure that all projects are using the same versions of libraries. This can help prevent compatibility issues between projects.
In Gradle 7.x or later, the version catalog is contained in a file libs.versions.toml
in your gradle project directory.
A version catalog file looks like this:
[versions]
guava = "32.1.3-jre"
junit-jupiter = "5.10.1"
[libraries]
guava = { module = "com.google.guava:guava", version.ref = "guava" }
junit-jupiter = { module = "org.junit.jupiter:junit-jupiter", version.ref = "junit-jupiter" }
In build.gradle.kts
, we can use these library names to refer to these specific libraries and versions.
dependencies {
// Use JUnit Jupiter for testing.
testImplementation(libs.junit.jupiter)
testRuntimeOnly("org.junit.platform:junit-platform-launcher")
// This dependency is used by the application.
implementation(libs.guava)
}
#
Last Word
It's staggering how much software is available through package repositories…