References: Convention Plugins | Version Catalog | KSP
Essential settings for KMP builds:
# gradle.properties
# Kotlin
kotlin.code.style=official
kotlin.mpp.androidSourceSetLayoutVersion=2
# Android
android.useAndroidX=true
android.nonTransitiveRClass=true
# Build performance
org.gradle.jvmargs=-Xmx4g -XX:+UseParallelGC
org.gradle.parallel=true
org.gradle.caching=true
org.gradle.configuration-cache=true
# Compose
org.jetbrains.compose.experimental.uikit.enabled=true
Always pin the Gradle version in gradle/wrapper/gradle-wrapper.properties:
distributionUrl=https\://services.gradle.org/distributions/gradle-8.10-bin.zip
Use Gradle 8.10+ with AGP 8.8+ and Kotlin 2.x.
For multi-module projects, extract all build logic into convention plugins to keep module build.gradle.kts files minimal and consistent.
build-logic/
└── convention/
├── build.gradle.kts
└── src/main/kotlin/
├── KmpLibraryPlugin.kt
├── KmpLibraryComposePlugin.kt
├── AndroidApplicationPlugin.kt
└── KoinPlugin.kt
plugins {
`kotlin-dsl`
}
dependencies {
compileOnly(libs.android.gradlePlugin)
compileOnly(libs.kotlin.gradlePlugin)
compileOnly(libs.compose.gradlePlugin)
compileOnly(libs.ksp.gradlePlugin)
}
// Register all convention plugins
gradlePlugin {
plugins {
register("kmpLibrary") {
id = "convention.kmp.library"
implementationClass = "KmpLibraryPlugin"
}
register("kmpLibraryCompose") {
id = "convention.kmp.library.compose"
implementationClass = "KmpLibraryComposePlugin"
}
register("androidApplication") {
id = "convention.android.application"
implementationClass = "AndroidApplicationPlugin"
}
register("koin") {
id = "convention.koin"
implementationClass = "KoinPlugin"
}
}
}
import com.android.build.gradle.LibraryExtension
import org.gradle.api.Plugin
import org.gradle.api.Project
import org.gradle.kotlin.dsl.configure
import org.gradle.kotlin.dsl.getByType
import org.jetbrains.kotlin.gradle.dsl.KotlinMultiplatformExtension
class KmpLibraryPlugin : Plugin<Project> {
override fun apply(target: Project) {
with(target) {
with(pluginManager) {
apply("org.jetbrains.kotlin.multiplatform")
apply("com.android.library")
}
extensions.configure<LibraryExtension> {
compileSdk = 35
defaultConfig {
minSdk = 26
}
compileOptions {
sourceCompatibility = JavaVersion.VERSION_17
targetCompatibility = JavaVersion.VERSION_17
}
}
extensions.configure<KotlinMultiplatformExtension> {
androidTarget {
compilations.all {
compileTaskProvider.configure {
compilerOptions {
jvmTarget.set(org.jetbrains.kotlin.gradle.dsl.JvmTarget.JVM_17)
}
}
}
}
listOf(iosX64(), iosArm64(), iosSimulatorArm64()).forEach { target ->
target.binaries.framework {
baseName = project.name
isStatic = true
}
}
sourceSets.commonMain.dependencies {
implementation(libs.findLibrary("kotlinx-coroutines-core").get())
}
}
}
}
}
import org.gradle.api.Plugin
import org.gradle.api.Project
import org.gradle.kotlin.dsl.configure
import org.jetbrains.compose.ComposeExtension
import org.jetbrains.kotlin.gradle.dsl.KotlinMultiplatformExtension
class KmpLibraryComposePlugin : Plugin<Project> {
override fun apply(target: Project) {
with(target) {
pluginManager.apply("convention.kmp.library")
pluginManager.apply("org.jetbrains.compose")
pluginManager.apply("org.jetbrains.kotlin.plugin.compose")
extensions.configure<KotlinMultiplatformExtension> {
sourceSets.commonMain.dependencies {
val compose = extensions.getByType<ComposeExtension>().dependencies
implementation(compose.runtime)
implementation(compose.foundation)
implementation(compose.material3)
implementation(compose.ui)
implementation(compose.components.resources)
}
}
}
}
}
import org.gradle.api.Plugin
import org.gradle.api.Project
import org.gradle.kotlin.dsl.configure
import org.jetbrains.kotlin.gradle.dsl.KotlinMultiplatformExtension
class KoinPlugin : Plugin<Project> {
override fun apply(target: Project) {
with(target) {
extensions.configure<KotlinMultiplatformExtension> {
sourceSets.commonMain.dependencies {
implementation(libs.findLibrary("koin-core").get())
implementation(libs.findLibrary("koin-compose").get())
implementation(libs.findLibrary("koin-compose-viewmodel").get())
}
sourceSets.named("androidMain") {
dependencies {
implementation(libs.findLibrary("koin-android").get())
}
}
}
}
}
}
// feature/home/build.gradle.kts
plugins {
alias(libs.plugins.convention.kmp.library.compose)
alias(libs.plugins.convention.koin)
alias(libs.plugins.kotlin.serialization)
}
kotlin {
sourceSets {
commonMain.dependencies {
implementation(projects.core.domain)
implementation(projects.core.data)
implementation(libs.navigation.compose)
}
}
}
// shared/build.gradle.kts
plugins {
alias(libs.plugins.ksp)
alias(libs.plugins.room)
}
kotlin {
sourceSets {
commonMain.dependencies {
implementation(libs.room.runtime)
implementation(libs.room.ktx)
}
}
}
// KSP — add Room processor for each platform target
dependencies {
add("kspAndroid", libs.room.compiler)
add("kspIosX64", libs.room.compiler)
add("kspIosArm64", libs.room.compiler)
add("kspIosSimulatorArm64", libs.room.compiler)
}
// Room — schema output directory for migration validation
room {
schemaDirectory("$projectDir/schemas")
}
Enable incremental processing in gradle.properties:
ksp.incremental=true
Add build plugin deps to the catalog so convention plugins can use libs:
# gradle/libs.versions.toml
[libraries]
android-gradlePlugin = { module = "com.android.tools.build:gradle", version.ref = "agp" }
kotlin-gradlePlugin = { module = "org.jetbrains.kotlin:kotlin-gradle-plugin", version.ref = "kotlin" }
compose-gradlePlugin = { module = "org.jetbrains.compose:compose-gradle-plugin", version.ref = "compose-multiplatform" }
ksp-gradlePlugin = { module = "com.google.devtools.ksp:symbol-processing-gradle-plugin", version.ref = "ksp" }
[plugins]
convention-kmp-library = { id = "convention.kmp.library", version = "unspecified" }
convention-kmp-library-compose = { id = "convention.kmp.library.compose", version = "unspecified" }
convention-android-application = { id = "convention.android.application", version = "unspecified" }
convention-koin = { id = "convention.koin", version = "unspecified" }
// settings.gradle.kts
pluginManagement {
includeBuild("build-logic") // include convention plugins
repositories {
google()
mavenCentral()
gradlePluginPortal()
}
}
dependencyResolutionManagement {
repositoriesMode.set(RepositoriesMode.FAIL_ON_PROJECT_REPOS)
repositories {
google()
mavenCentral()
}
}
rootProject.name = "MyApp"
// Feature modules
include(":app")
include(":iosApp")
include(":shared")
include(":core:domain")
include(":core:data")
include(":core:database")
include(":core:network")
include(":core:designsystem")
include(":feature:home:api")
include(":feature:home:impl")
include(":feature:auth:api")
include(":feature:auth:impl")
// shared/build.gradle.kts
buildkonfig {
packageName = "com.example.shared"
defaultConfigs {
buildConfigField(STRING, "ENVIRONMENT", "staging")
buildConfigField(BOOLEAN, "IS_DEBUG", "true")
buildConfigField(BOOLEAN, "ENABLE_LOGGING", "true")
buildConfigField(STRING, "API_BASE_URL", "https://api.staging.example.com")
buildConfigField(STRING, "APP_VERSION", project.version.toString())
}
// Production overrides
targetConfigs("prod") {
buildConfigField(STRING, "ENVIRONMENT", "production")
buildConfigField(BOOLEAN, "IS_DEBUG", "false")
buildConfigField(BOOLEAN, "ENABLE_LOGGING", "false")
buildConfigField(STRING, "API_BASE_URL", "https://api.example.com")
}
}
Access from Swift:
import shared
let apiUrl = BuildKonfig.shared.API_BASE_URL
let isDebug = BuildKonfig.shared.IS_DEBUG
Access from Kotlin:
val apiUrl = BuildKonfig.API_BASE_URL
val isDebug = BuildKonfig.IS_DEBUG
Keep rules are required for Ktor, Room, and Koin in release builds. Add to android/proguard-rules.pro:
# Ktor — keep serialization metadata
-keep class io.ktor.** { *; }
-keep class kotlinx.serialization.** { *; }
-keepattributes *Annotation*, InnerClasses
-dontnote kotlinx.serialization.AnnotationsKt
-keepclassmembers class ** {
@kotlinx.serialization.Serializable *;
}
# Room — keep generated _Impl classes
-keep class * extends androidx.room.RoomDatabase
-keep @androidx.room.Entity class *
-dontwarn androidx.room.paging.**
# Koin — keep module declarations
-keep class org.koin.** { *; }
-keepnames class * extends org.koin.core.module.Module
# Kotlin reflection (used by Koin)
-keep class kotlin.Metadata { *; }
-keepclassmembers class ** {
@kotlin.jvm.JvmField *;
@kotlin.jvm.JvmStatic *;
}
# DataStore
-keep class androidx.datastore.** { *; }
# Coroutines
-keepnames class kotlinx.coroutines.internal.MainDispatcherFactory {}
-keepnames class kotlinx.coroutines.CoroutineExceptionHandler {}
Enable R8 full mode for better size reduction (requires more explicit keeps):
// android/build.gradle.kts
android {
buildTypes {
release {
isMinifyEnabled = true
isShrinkResources = true
proguardFiles(
getDefaultProguardFile("proguard-android-optimize.txt"),
"proguard-rules.pro"
)
}
}
}
# gradle.properties — enable R8 full mode
android.enableR8.fullMode=true
// shared/build.gradle.kts
plugins {
id("maven-publish")
}
afterEvaluate {
publishing {
publications {
create<MavenPublication>("release") {
groupId = "com.example"
artifactId = "shared"
version = "1.0.0"
from(components["kotlin"])
}
}
repositories {
maven {
name = "LocalMaven"
url = uri("${rootProject.buildDir}/local-maven")
}
}
}
}
# Publish to local Maven repo
./gradlew :shared:publishReleasePublicationToLocalMavenRepository
# Consume from local Maven in another project
repositories {
maven { url = uri("/path/to/local-maven") }
}
// shared/build.gradle.kts
publishing {
repositories {
maven {
name = "GitHubPackages"
url = uri("https://maven.pkg.github.com/ORG/REPO")
credentials {
username = System.getenv("GITHUB_ACTOR")
password = System.getenv("GITHUB_TOKEN")
}
}
}
}
# .github/workflows/publish.yml
- name: Publish to GitHub Packages
run: ./gradlew :shared:publish
env:
GITHUB_ACTOR: ${{ github.actor }}
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
Disable the Gradle daemon in CI to avoid warm-up overhead and avoid orphaned daemon processes:
# gradle.properties (CI override via environment or -P flag)
# In CI, set via: ./gradlew -Dorg.gradle.daemon=false
org.gradle.daemon=false
Or set in CI workflow:
# GitHub Actions — disable daemon and configure memory for CI
env:
GRADLE_OPTS: "-Dorg.gradle.daemon=false -Dkotlin.incremental=false -Dorg.gradle.jvmargs=-Xmx4g"
Use Gradle's built-in caching in GitHub Actions:
- name: Setup Gradle
uses: gradle/actions/setup-gradle@v4
with:
cache-encryption-key: ${{ secrets.GRADLE_ENCRYPTION_KEY }}
- name: Build
run: ./gradlew :shared:assembleRelease
This caches the Gradle home directory, build cache, and configuration cache between runs — significantly speeds up CI.
org.gradle.caching=true in gradle.propertiesorg.gradle.configuration-cache=trueorg.gradle.parallel=trueimplementation in api — Use api() only for types exposed in public signaturescompileOnly for annotation processors that don't need to be on runtime classpathtestImplementation over implementation for test-only depsorg.gradle.daemon=false via GRADLE_OPTS environment variable--no-configuration-cache selectively — some third-party plugins are not configuration-cache compatible yet; add them to the incompatible task list rather than disabling globallyCheck build health with:
./gradlew buildHealth # dependency analysis
./gradlew :shared:dependencies # inspect dependency tree
./gradlew --profile # generate HTML build performance report