Added a comprehensive collection of Compose development guidelines, best practices, and skill definitions in the .codex directory. Co-authored-by: Junie <junie@jetbrains.com>
8.2 KiB
Gradle & Build Configuration
Gradle best practices for Compose Multiplatform (CMP) and Android-only Jetpack Compose projects, including AGP 9+ changes.
1. Project Structure Patterns
CMP Project (Android + iOS + optional Desktop)
MyApp/
├── settings.gradle.kts
├── build.gradle.kts # Root: plugins with apply false
├── gradle.properties
├── gradle/libs.versions.toml
├── composeApp/ # KMP shared library
│ └── src/{commonMain,androidMain,iosMain,jvmMain}
├── androidApp/ # Thin Android shell (required by AGP 9+)
├── desktopApp/ # Optional: Desktop JVM entry point
└── iosApp/ # Xcode project (NOT a Gradle module)
Key points:
composeAppis a KMP library containing all shared codeandroidAppis a thin shell — AGP 9'scom.android.applicationcannot coexist with KMP pluginiosAppis a standalone Xcode project, not a Gradle module
Android-Only Project
MyApp/
├── settings.gradle.kts
├── build.gradle.kts
├── gradle/libs.versions.toml
├── app/ # Main application module
├── feature-*/ # Feature modules
└── core-*/ # Shared modules (ui, data, domain)
2. Version Catalog (libs.versions.toml)
Four sections: [versions], [libraries], [plugins], [bundles]. Use comment headers to group by domain.
[versions]
# ---- Build ----
agp = "9.0.1"
kotlin = "2.3.10"
ksp = "2.3.10-1.0.30"
compose-multiplatform = "1.10.1"
# ---- AndroidX ----
androidx-lifecycle = "2.9.1"
# ---- Networking ----
ktor = "3.2.0"
[libraries]
# BOM-managed libs omit version.ref
compose-bom = { module = "androidx.compose:compose-bom", version = "2026.03.00" }
compose-material3 = { module = "androidx.compose.material3:material3" }
# Regular libs use version.ref
ktor-client-core = { module = "io.ktor:ktor-client-core", version.ref = "ktor" }
[plugins]
android-application = { id = "com.android.application", version.ref = "agp" }
android-kmp-library = { id = "com.android.kotlin.multiplatform.library", version.ref = "agp" }
kotlin-multiplatform = { id = "org.jetbrains.kotlin.multiplatform", version.ref = "kotlin" }
compose-multiplatform = { id = "org.jetbrains.compose", version.ref = "compose-multiplatform" }
compose-compiler = { id = "org.jetbrains.kotlin.plugin.compose", version.ref = "kotlin" }
Naming: kebab-case keys → dot accessors (koin-core → libs.koin.core). BOM-managed libraries omit version.ref. Use # ---- Section ---- comment headers to visually group entries by domain.
3. Bundles ([bundles])
[bundles] groups libraries always added together into one alias — convenience only; no change to resolution or alignment. Create bundles when two+ libs are added as a set; group by domain and use comment headers like [versions]/[libraries].
implementation(libs.bundles.androidx.base)
implementation(libs.bundles.androidx.lifecycle)
CMP projects rarely need bundles because commonMain.dependencies already groups everything in one place.
4. settings.gradle.kts
rootProject.name = "MyApp"
enableFeaturePreview("TYPESAFE_PROJECT_ACCESSORS")
pluginManagement {
repositories {
google { content { includeGroupByRegex("com\\.android.*|com\\.google.*|androidx.*") } }
mavenCentral()
gradlePluginPortal()
}
}
dependencyResolutionManagement {
repositoriesMode.set(RepositoriesMode.FAIL_ON_PROJECT_REPOS)
repositories {
google { content { includeGroupByRegex("com\\.android.*|com\\.google.*|androidx.*") } }
mavenCentral()
}
}
include(":composeApp", ":androidApp")
5. Root build.gradle.kts
Declare plugins with apply false. No allprojects {}/subprojects {} — use convention plugins at scale.
plugins {
alias(libs.plugins.android.application) apply false
alias(libs.plugins.android.kmp.library) apply false
alias(libs.plugins.kotlin.multiplatform) apply false
alias(libs.plugins.compose.multiplatform) apply false
alias(libs.plugins.compose.compiler) apply false
alias(libs.plugins.ksp) apply false
}
6. AGP 9+ Changes
Built-in Kotlin
AGP 9 includes Kotlin. Do NOT apply org.jetbrains.kotlin.android in Android app modules.
// ✅ AGP 9+
plugins {
alias(libs.plugins.android.application)
alias(libs.plugins.compose.compiler)
}
New KMP Library Plugin
Use com.android.kotlin.multiplatform.library for KMP modules targeting Android.
// ✅ AGP 9+ KMP module
plugins {
alias(libs.plugins.kotlin.multiplatform)
alias(libs.plugins.android.kmp.library)
}
New compileSdk DSL
// Application modules
android {
compileSdk { version = release(35) }
}
// KMP library modules (inside kotlin { androidLibrary {} })
kotlin {
androidLibrary {
compileSdk = 35 // Integer still works here
}
}
Kotlin Block Outside Android
On AGP 9+, kotlin {} must NOT be nested inside android {}.
// ✅ Correct
kotlin { jvmToolchain(21) }
android { /* ... */ }
// ❌ Wrong
android { kotlin { jvmToolchain(21) } }
7. Module Patterns
CMP Shared Module (composeApp)
plugins {
alias(libs.plugins.kotlin.multiplatform)
alias(libs.plugins.android.kmp.library)
alias(libs.plugins.compose.multiplatform)
alias(libs.plugins.compose.compiler)
alias(libs.plugins.ksp)
}
kotlin {
androidLibrary {
namespace = "com.example.shared"
compileSdk = 35
minSdk = 26
}
listOf(iosArm64(), iosSimulatorArm64()).forEach {
it.binaries.framework {
baseName = "ComposeApp"
isStatic = true
}
}
sourceSets {
commonMain.dependencies {
implementation(compose.runtime)
implementation(compose.material3)
// Add other common dependencies
}
}
}
dependencies {
listOf("kspAndroid", "kspIosArm64", "kspIosSimulatorArm64").forEach {
add(it, libs.room.compiler)
}
}
Android app module: thin shell with com.android.application + compose-compiler plugins, depending on projects.composeApp.
Desktop module: KMP plugin + compose.desktop.currentOs, entry point via compose.desktop { application { mainClass = "..." } }.
8. gradle.properties
# Performance
org.gradle.configuration-cache=true
org.gradle.caching=true
org.gradle.parallel=true
org.gradle.jvmargs=-Xmx4g -XX:+UseParallelGC
# Kotlin
kotlin.code.style=official
# Android
android.useAndroidX=true
android.nonTransitiveRClass=true
# CMP (if targeting iOS)
kotlin.mpp.enableCInteropCommonization=true
9. KSP Wiring
dependencies {
listOf("kspAndroid", "kspIosArm64", "kspIosSimulatorArm64").forEach {
add(it, libs.room.compiler)
add(it, libs.koin.ksp.compiler)
}
}
ksp {
arg("KOIN_USE_COMPOSE_VIEWMODEL", "true")
}
tasks.withType<KotlinCompile>().configureEach {
dependsOn(tasks.withType<KspTask>())
}
10. Composite Builds
Conditional includeBuild for local library dev (use if (path.exists()) so CI works without checkout):
// settings.gradle.kts
val localLibPath = file("../my-library")
if (localLibPath.exists()) {
includeBuild(localLibPath) {
dependencySubstitution {
substitute(module("com.example:my-library")).using(project(":my-library"))
}
}
}
11. Convention Plugins
Introduce convention plugins when 3+ modules duplicate config. Use build-logic/ included build pattern. Not needed for small projects (≤3 modules).
12. Do / Don't
| Do | Don't |
|---|---|
| Version catalog for all dependencies | Hardcode versions in build files |
| Enable configuration cache, build cache | Use buildSrc for versions |
TYPESAFE_PROJECT_ACCESSORS |
allprojects {}/subprojects {} blocks |
Separate androidApp from KMP shared (AGP 9+) |
Apply kotlin-android on AGP 9+ |
apply false at root |
Nest kotlin {} inside android {} |
Conditional includeBuild for local dev |
Unconditional includeBuild (breaks CI) |
| Convention plugins for 3+ modules | Over-engineer small projects |