TOML + Gradle + project accessors
Intro
The new and shiny feature by Gradle is their way of having conventional dependencies that are organized in a fashionable and easy to grasp manner, now being stable in Gradle 7.4.2
, the TOML libs.versions
a.k.a version catalogs are the way to handle dependencies instead of hardcoding the artifacts in your existing Gradle scripts.
Before we get started
- Make sure you have your
distributionUrl
targeting at leastgradle-7.4.2-bin.zip
, otherwise up until that version you have to addenableFeaturePreview("VERSION_CATALOGS")
insidesettings.gradle
- Create a
libs.versions.toml
file inside your gradle folder
Understanding the TOML structure
There are four things to know when looking inside a TOML
version catalog file
[versions]
- where you would have the version number/s of your dependencies[libraries]
- kind of self explanatory, the libraries you would include later on and useversion.ref
to obtain the version fromversions
[bundles]
- you can group together multiplelibraries
from .2[plugins]
- where the plugins that are being added to the project live
TOML
is minimal language and is designed to map unambiguously to a hash table, this makes it easy to couple it with a Gradle script, however do note that you can add tons of stufs in that .toml file that would make them easier for your development process later on, you can check the additional specs of what more could be added.
For the sake of this article and brevity we would keep it short and what you may use daily
How?
Let’s go on by adding few versions of what would be our dependency handler later on.
Open the libs.versions.toml
file inside your gradle folder that you created before you started.
First we add versions for each library we’d use
1
2
3
4
5
[versions]
kotlin = "1.6.21"
coroutines = "1.6.1"
kaHelpers = "3.4.0"
gradlePlugins-agp = "7.2.0"
We include the libraries
1
2
3
4
[libraries]
coroutines-core = { module = "org.jetbrains.kotlinx:kotlinx-coroutines-core", version.ref = "coroutines" }
coroutines-android = { module = "org.jetbrains.kotlinx:kotlinx-coroutines-android", version.ref = "coroutines" }
kahelpers-toaster = { module = "com.github.FunkyMuse.KAHelpers:toaster", version.ref = "kaHelpers" }
now as you can see we, have two libraries that makes sense to group them we use bundles to do so
1
2
[bundles]
coroutines = ["coroutines-android", "coroutines-core"]
as aforementioned that the TOML
is basically a hash table, bundles
can look up libraries by the name (key) you declared, just as version.ref
does for versions
.
Since we added versions for plugins we can utilise the plugins
section
1
2
3
4
[plugins]
android = { id = "com.android.application", version.ref = "gradlePlugins-agp" }
kotlinAndroid = { id = "org.jetbrains.kotlin.android", version.ref = "kotlin" }
kapt = { id = "org.jetbrains.kotlin.kapt", version.ref = "kotlin" }
The plugins section is gonna help us handle the plugins that are being added to the project without lots of boilerplate.
Our complete version catalog file ends up looking like
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
[versions]
kotlin = "1.6.21"
coroutines = "1.6.1"
kaHelpers = "3.4.0"
gradlePlugins-agp = "7.2.0"
[libraries]
coroutines-core = { module = "org.jetbrains.kotlinx:kotlinx-coroutines-core", version.ref = "coroutines" }
coroutines-android = { module = "org.jetbrains.kotlinx:kotlinx-coroutines-android", version.ref = "coroutines" }
kahelpers-toaster = { module = "com.github.FunkyMuse.KAHelpers:toaster", version.ref = "kaHelpers" }
[bundles]
coroutines = ["coroutines-android", "coroutines-core"]
[plugins]
android = { id = "com.android.application", version.ref = "gradlePlugins-agp" }
kotlinAndroid = { id = "org.jetbrains.kotlin.android", version.ref = "kotlin" }
kapt = { id = "org.jetbrains.kotlin.kapt", version.ref = "kotlin" }
Usage
Gradle allows really easy way to use the version catalog inside your scripts, you can access everything using libs.
It’s really simple, you can now use
1
2
implementation(libs.bundles.coroutines)
implementation(libs.kahelpers.toaster)
Each -
in the libraries name is replaced with .
inside the script, it’s like having a domain package name structure for the libraries.
As we progress towards the plugins section, inside your top level gradle build.gradle.kts
script you can declare your plugins
1
2
3
4
5
plugins {
alias(libs.plugins.android).apply(false)
alias(libs.plugins.kotlinAndroid).apply(false)
alias(libs.plugins.kapt).apply(false)
}
we’re using aliases because we’re gonna reference them from the gradle plugin portal instead of manually adding them to the buildScript
classpath.
Inside your settings.gradle.kts
make sure you have gradlePluginPortal()
1
2
3
4
5
6
7
pluginManagement {
repositories {
gradlePluginPortal() // <- this
google()
mavenCentral()
}
}
this pulls all the plugins from the gradle portal by their alias ID and adds them manually to the build script until you apply them later on wherever needed, this is why they’re not applied now since they live in the top-level gradle script.
Now inside your :app
module you can enable the plugins you’ve just added
1
2
3
4
5
plugins {
alias(libs.plugins.android)
alias(libs.plugins.kotlinAndroid)
alias(libs.plugins.kapt)
}
Thanks to Atul for improving the alias for the :app
module.
Project accessors
Since the version catalog is a powerful feature that one can utilise for a modern architecture where the project isn’t monolithic you need to gather some of your features inside the :app
module, let’s say in order to call them there (do note that this can vary from project to project since every project has different structure, this is only for demonstrations).
Tot enable the feature, inside your gradle.settings.kts
add this line
1
enableFeaturePreview("TYPESAFE_PROJECT_ACCESSORS")
so now instead of typing
1
implementation project(":feature_home")
you can use
1
implementation(projects.feature_home)
Drawbacks of using version catalogs
As of this article’s date, there are several drawbacks that you need to be aware when using version catalogs for your projects
- There’s no auto completion by the IDE (yet)
- There’s no auto suggestion to update the versions
- Dependabot still hasn’t support for TOML with Gradle, there’s an issue but who knows, the alternative way is using renovate or a custom Github cron job that can update the versions
- It doesn’t control transitive dependencies, a catalog only references direct dependencies
With this in mind and as mentioned in the beginnig, treat the version catalog as a catalog for discoverability and easier maintenance.
Updating versions using community plugins
Since some of the aforementioned limitations are annoying, the community has presented us with solutions. There’s a wonderful plugin that would enable us to manually update the version catalog via a gradle task.
Our final version catalog ends up looking like this
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
[versions]
kotlin = "1.6.21"
coroutines = "1.6.1"
kaHelpers = "3.4.0"
gradlePlugins-agp = "7.2.0"
gradlePlugins-versionCatalog = "0.3.1"
[libraries]
coroutines-core = { module = "org.jetbrains.kotlinx:kotlinx-coroutines-core", version.ref = "coroutines" }
coroutines-android = { module = "org.jetbrains.kotlinx:kotlinx-coroutines-android", version.ref = "coroutines" }
kahelpers-toaster = { module = "com.github.FunkyMuse.KAHelpers:toaster", version.ref = "kaHelpers" }
[bundles]
coroutines = ["coroutines-android", "coroutines-core"]
[plugins]
android = { id = "com.android.application", version.ref = "gradlePlugins-agp" }
kotlinAndroid = { id = "org.jetbrains.kotlin.android", version.ref = "kotlin" }
kapt = { id = "org.jetbrains.kotlin.kapt", version.ref = "kotlin" }
versionCatalogUpdate = { id = "nl.littlerobots.version-catalog-update", version.ref = "gradlePlugins-versionCatalog" }
so that our top level gradle build.gradle.kts
looks like this
1
2
3
4
5
6
plugins {
alias(libs.plugins.android).apply(false)
alias(libs.plugins.kotlinAndroid).apply(false)
alias(libs.plugins.kapt).apply(false)
alias(libs.plugins.versionCatalogUpdate)
}
we can update the version numbers by running a simple script
1
./gradlew versionCatalogUpdate
You can now use version catalogs and project accessors to enable modern way of keeping your dependencies clear and access the sub-projects in fashionable and readable manner, this article doesn’t provide a way to build a Github action to create a dependabot auto update of the versions but you have all the required knowledge to create one for your project.
Goodbye
Don’t forget to drink water, it’s starting to look like summer and it’s hot (just as your machine running a build using Gradle).
This is my first article in 2022 as i’ve been spending this year learning Compose and Ktor backend, mentoring my friend and spending quality time on my passion projects.
Here’s a picture of two red pandas to make your day more awesome.