modulator – a lean Gradle plugin that gives Kotlin Multiplatform the superpower of JVM-style compileOnly
dependencies! No bloat, no dirty tricks, just clean, modular APIs, and full toolchain compatibility – forever!
Imagine a Spring Boot core with optional persistence: JPA/Hibernate or MongoDB. You want expressive extension functions
like Order.toJpaEntity()
or Order.toMongoDocument()
to become available automatically when the corresponding starter
is on the classpath, without forcing every service to depend on both stacks.
On the JVM you’d create these adapters using compileOnly
dependencies on JPA/Hibernate and MongoDB to keep your core clean.
Unfortunately, Kotlin Multiplatform says no!
Hence, optional, extension‑driven integrations either bloat dependency graphs or require tedious manual wiring.
Enter modulator – a lean Gradle plugin that brings two complementary capabilities to Kotlin Multiplatform:
- Piggyback modules with extension functionality and/or glue code on two or more
carrier
modules within a multi-module project. - Automatically add those piggybacked modules as dependencies when all of their carriers are present in a consuming project.
Just apply at.asitplus.gradle.modulator
to any Gradle module that requires either capability.
That’s it – no custom wiring, no dependency clutter, no hacks, no compiler plugins, no code generation,
but full backwards compatibility with all Kotlin and Gradle tooling!
modulator introduces a new type of dependency: carrier
dependencies, that are available alongside api
, implementation
, and so forth.
A bridge / glue module depends on two or more carrier modules (within the same multi-module gradle project).
When all carriers are present in a consumer, the bridge module is automatically pulled in.
If bridgeModule
should provide glue functionality between modA
and modB
- apply the
at.asitplus.gradle.modulator
Gradle plugin - add
modA
andmodB
ascarrier
dependencies:
//build.gradle.kts of bridgeModule
plugins {
alias(libs.plugins.kotlinMultiplatform)
`maven-publish`
/* …… */
id("at.asitplus.gradle.modulator") version "$modulatorVersion"
}
kotlin {
listOf(
iosX64(),
iosArm64(),
iosSimulatorArm64()
).forEach { iosTarget ->
iosTarget.binaries.framework {
baseName = "BridgeModule"
isStatic = true
}
}
jvm()
// add additional targets as desired
sourceSets {
//does not have to be commonMain, but it makes the most sense
commonMain.dependencies {
carrier(project(":modA")) //no need to modify modA's buildscript
carrier(project(":modB")) //no need to modify modB's buildscript
}
}
}
//…… publishing, etc.
This will add metadata to both modA
and modB
publications, such that the published artifacts of both contain the information that
bridgeModule
should be pulled in when both modA
and modB
are added as dependencies to a consuming project.
The buildscripts of neither modA
nor modB
require any changes or even the modulator gradle plugin.
modulator works its magic in consuming projects even less obtrusively:
Just apply the modulator Gradle plugin in consumers and the carrier dependencies as regular api
or implementation
dependencies.
No other changes are required to the buildscript.
//build.gradle.kts of consuming project
plugins {
alias(libs.plugins.kotlinMultiplatform)
id("at.asitplus.gradle.modulator") version "$modulatorVersion"
}
kotlin {
listOf(
iosX64(),
iosArm64(),
iosSimulatorArm64()
).forEach { iosTarget ->
iosTarget.binaries.framework {
baseName = "ComposeApp"
isStatic = true
}
}
jvm()
// add additional targets as desired
sourceSets {
commonMain.dependencies {
api("com.example.modA:$modAversion")
api("com.example.modB:$modBversion")
//that's it! bridgeModule will be automagically pulled in
}
}
}
If modA
and modB
are added as api
dependencies, the bridge module will also be added as api
dependency. The same holds
for implementation
dependencies.
For library authors, this is not quite as hassle-free as compileOnly
dependencies on the JVM but:
- The project setup remains fully transparent, predictable, intelligible and easily maintainable.
- The use of dedicated bridge modules and enriched Gradle metadata on carrier modules is fully and perfectly backwards-compatible with the whole Gradle/KMP ecosystem, and it will stay that way.
- Your project either compiles or it does not run. No
RuntimeException
or other unpleasant surprises, because everything is known at compile-time.
In the end, no invasive changes to the KMP/Gradle tooling are required, as modulator simply adds additional dependencies in the same way as adding them explicitly yourself.
The example
directory contains two projects that showcase modulator:
modulatingProducer
contains three modules:cose
providing a single sample COSE-ish data classjose
providing a single sample JOSE-ish data classcoseToJose
providing mapper functionality from COSE to JOSE
modulatedConsumer
contains a single module that addscose
andjose
dependencies and uses the mapping functionality provided bycosetoJose
, showcasing that no explicit adding of this dependency is needed
To try it out: publish modulatingProduce
to maven local and open modulatedConsumer
in IDEA to witness the magic!
The Apache License does not apply to the logos, (including the A-SIT logo) and the project/module name(s), as these are the sole property of A-SIT/A-SIT Plus GmbH and may not be used in derivative works without explicit permission!