KiteConfig¶
One place to declare a Kotlin Multiplatform app's identity: name, version, bundle ID, locales and SDK levels. KiteConfig propagates it to the Android, Xcode and Kotlin files that each normally keep their own copy.
A typical Kotlin Multiplatform repo records its app name and version in four places:
- the Android module's
defaultConfig - the Xcode project's build settings
- the source
Info.plist - whatever constant the shared Kotlin code reads
Nothing keeps those four copies in agreement, so eventually they stop matching.
The shortest setup that does something¶
In the root build.gradle.kts:
plugins {
kotlin("multiplatform") version "2.4.21" apply false
id("com.android.application") version "9.5.0" apply false
id("io.github.yuroyami.kiteconfig") version "3.1.0"
}
kiteConfig {
appName = "Jetzy"
appId = "com.example.jetzy"
version = "1.4.0"
autoApply = true
android {
sdk(min = 26, target = 36, compile = 36)
}
}
Then:
That prints the resolved model and writes nothing. Run it again whenever a value is not where you expected it.
Two required preconditions
The plugin goes on the root project. Applying it in a submodule throws
immediately, because it aggregates across allprojects from the root.
Add apply false to the Kotlin and Android plugin lines. KiteConfig
integrates with typed classes from KGP (the Kotlin Gradle plugin) and AGP
(the Android Gradle plugin). Those integrations run only when KiteConfig can
load the plugin classes from its own classloader. Declare
kotlin("multiplatform") only inside a subproject and Gradle loads KGP with
a different classloader. KiteConfig cannot read the plugin classes from there.
One switch¶
Gradle configuration is automatic and continuous. On every build, KiteConfig
applies the Android identity and SDK levels, aligns the Java and Kotlin bytecode
levels, and generates Kotlin under build/. This happens inside AGP's
finalizeDsl hook, which runs after a module's own android { } block, so a
value set in kiteConfig { } replaces the same value set in the module.
Changes to files you commit are what autoApply decides.
project.pbxproj, Info.plist, Podfile, Swift imports and launcher icons are
yours. With autoApply = true, an ordinary build applies the ones your
configuration needs. With it off, nothing touches them until you run
./gradlew kiteApply.
With iOS automatic application enabled, Android Studio and IntelliJ Gradle sync
also applies the iOS plan through Kotlin's IDE import task. Sync after changing
KiteConfig so Xcode reads the new version, build number and assets before its
next build. Opening Xcode alone does not run Gradle. For changes made outside
the Gradle IDE, run ./gradlew kiteApplyIos before archiving.
There is nothing else to authorize. A feature is on when you gave it what it
needs: set logo { foreground } and the icons are installed, on every platform
that is still selected. A feature that needs nothing from you, like the launch
screen, asks for enabled = true.
Every source change goes through the same rails: containment, ownership receipts, checksums, a recovery copy on first contact, staged writes, and rollback. Nothing is written until everything has been rendered and checked.
Run ./gradlew kitePlan first. It lists exactly what would change, with paths,
and writes nothing. dryRun = true does the same for the tasks themselves.
Where things live¶
README The full guide: the whole surface, the task table, and how to upgrade.
API reference Every extension property and task type, generated from source.
FEATURES.md Behavior reference: what each switch does, and every default value.
Changelog Release history, including the upgrade from 1.0.0.
Reading values back¶
Everything KiteConfig resolves is readable from any build file in the project, not just the root. One import, then use it:
import io.github.yuroyami.kiteconfig.kiteConfig
android {
defaultConfig {
versionCode = kiteConfig.versionCode.get()
}
}
| Group | Values |
|---|---|
| Version | version, versionCode, iosBuildNumber, iosMarketingVersion, desktopBuildNumber |
| Identity | appName, appNameFor(platform), appId, appIdFor(platform), androidApplicationId, iosBundleId, desktopBundleId |
| Build | canonicalLocales, jvmTarget, jvmToolchain, resolvedSharedProjectPath, minSdk, targetSdk, compileSdk, ndk |
Every one is a lazy Provider, so wiring one into another task's property costs
nothing at configuration time:
These accessors supply no defaults and never return null. A value the root build
file never set has no value at all, and reading it stops the build. That is on
purpose: quietly falling back to something like ?: 24 would put a second copy
of that number in the consumer, which is the duplication this plugin exists to
remove.
Reading across projects means this is not compatible with Gradle Isolated Projects. Neither is the rest of the plugin.
When values resolve¶
Most values are settled before any subproject build file runs, so reading them eagerly during configuration is safe:
Two resolve later, because they depend on inspecting every project first:
| Value | Reading it eagerly |
|---|---|
canonicalLocales |
returns an empty list, unless you set locales { tags } |
resolvedSharedProjectPath |
has no value, unless modules { shared } is declared |
Neither affects what the build itself uses; the cost is only to the caller that asked too early. Wire them into a task instead and let them resolve at execution time:
Compatibility¶
Gradle 8.5 and newer, AGP 8.5.2 through 9.5.x, KGP 2.4.x, Compose 1.11 through 1.13.x, on a JDK 17 or 21 daemon.
A dedicated agpCompatibilityTest builds real consumer projects on Gradle 8.5,
8.9 and 9.9.0-rc-1, against AGP 8.5.2 and 9.5.0. CI runs on Linux with JDK 17
and 21, on macOS with JDK 21, and on Windows with JDK 21. Each build runs twice.
CI then checks that the second run reuses the configuration cache entry, and
that no tracked file changed.