Skip to content

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:

./gradlew kiteVerify

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

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:

someOtherTask.someProperty.set(kiteConfig.androidApplicationId)

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:

versionCode = kiteConfig.versionCode.get()

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:

someTask.localeList.set(kiteConfig.canonicalLocales)

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.