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
carriermodules 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.modulatorGradle plugin - add
modAandmodBascarrierdependencies:
//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
RuntimeExceptionor 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:
modulatingProducercontains three modules:coseproviding a single sample COSE-ish data classjoseproviding a single sample JOSE-ish data classcoseToJoseproviding mapper functionality from COSE to JOSE
modulatedConsumercontains a single module that addscoseandjosedependencies 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!