Compose Multiplatform in Action: Understanding CMP Project Structure and Build Configuration
Compose Multiplatform (CMP)
Yesterday we just finished configuring the simulators for CMP
I’ve been meaning to mention
that I’m hoping this series of articles
can help beginners
and even people who don’t typically develop Mobile Apps
to get started
so I’m being quite detailed in some places
I hope you’ll bear with me XDDD
Today I’ll introduce the CMP project structure
along with Gradle configuration
adjustments
and the purpose of lib.version.toml
in CMP projects
目錄
- Compose Multiplatform 實戰:放輕鬆點,初探CMP
- Compose Multiplatform 實戰:初戰,安裝CMP環境吧
- Compose Multiplatform 實戰:續戰,用Wizard創建CMP專案
- Compose Multiplatform 實戰:在Android、iOS模擬器上跑CMP專案
- Compose Multiplatform 實戰:CMP的專案結構理解與編譯配置
- Compose Multiplatform 實戰:CMP中跨平台Android、iOS程式碼的進入點
- Compose Multiplatform 實戰:在CMP的Compose中用Material Design3 Theme
- Compose Multiplatform 實戰:CMP用Compose實作跨平台畫面
- Compose Multiplatform 實戰:使用 expect 和 actual 實現跨平台程式碼
- Compose Multiplatform 實戰:CMP中實作Compose Navigation頁面切換
- Compose Multiplatform 實戰:CMP中透過StateFlow來管理UI狀態
- Compose Multiplatform 實戰:CMP中實作NavigationBar底部欄
- Compose Multiplatform 實戰:CMP中使用koin來依賴注入Dependency Injection
- Compose Multiplatform 實戰:CMP實作跨平台資料庫SqlDelight
- Compose Multiplatform 實戰:CMP中使用ROOM開發跨平台資料庫 & 疑難雜症
Here is the default project structure
when you create a CMP project
YourProjectName
├── build
├── composeApp
│ ├── build
│ ├── src
│ │ ├── commonMain
│ │ ├── commonTest
│ │ ├── iosMain
│ │ └── desktopMain
│ └── build.gradle.kts
├── gradle
│ ├── wrapper
│ │ └── libs.versions.toml
├── iosApp
│ ├── iosApp
│ └── iosApp.xcodeproj
├── .gitignore
├── build.gradle.kts
├── gradle.properties
├── local.properties
└── settings.gradle.kts
Here’s a breakdown of the purpose of each folder and file
to help beginners understand more quickly
YourProjectName
: The project name and the root folder of the entire projectbuild
: Output files from the compilation process, generated during build, so it can be added to.gitignore
composeApp
: Contains the source code and configuration for the Compose Multiplatform applicationbuild
: Build outputs forCMP
, also generated during compilation, so it can be added to.gitignore
src
: The directory forCMP
codecommonMain
: Directory for common logic code in the CMP projectcommonTest
: Directory for test code in the CMP projectiosMain
: Directory foriOS
implementation codedesktopMain
: Directory fordesktop
implementation code
build.gradle.kts
: Gradle configuration file forCMP
gradle
: Gradle-related configuration fileswrapper
: Contains Gradle Wrapper related fileslibs.versions.toml
: Defines thedependency versions
used in the project
iosApp
: Root folder for the iOS projectiosApp
: Directory for iOS code
iosApp.xcodeproj
: Xcode project file for iOS.gitignore
: Defines which files or directories should be ignored in Git version controlbuild.gradle.kts
: Gradle configuration file for theroot directory
gradle.properties
: Gradle properties filelocal.properties
: Properties file defining local configurationssettings.gradle.kts
: Gradle file defining the settings for theCMP
project
libs.versions.toml
is a configuration file used to manage project dependency versions
especially in projects built with Gradle
According to the official Gradle documentation
this feature was supported in the Gradle 7.0
release
and they call this feature version catalogs
This file uses the TOML (Tom’s Obvious, Minimal Language) format to define dependency version information
centralizing this information in the project to improve maintainability and readability
Here’s an example of a libs.versions.toml file and its explanation:
[versions]
agp = "8.2.0"
kotlin = "2.0.10-RC"
androidx-activityCompose = "1.9.0"
[libraries]
androidx-activity-compose = { module = "androidx.activity:activity-compose", version.ref = "androidx-activityCompose" }
[plugins]
androidApplication = { id = "com.android.application", version.ref = "agp" }
androidLibrary = { id = "com.android.library", version.ref = "agp" }
kotlinMultiplatform = { id = "org.jetbrains.kotlin.multiplatform", version.ref = "kotlin" }
Key sections explained
:
-
Version definitions
[versions]
:
This section defines the version numbers of various dependencies used in the project
For example, kotlin = “2.0.10-RC” indicates that the Kotlin version is 2.0.10-RC. - Dependency definitions
[libraries]
:
This section defines the actual dependencies used in the project and their version information
Each library defines amodule
and aversion.ref
wheremodule
is the dependency’sMaven
coordinate
andversion.ref
references the version number defined above
- For example, androidx-activity-compose = { module = “androidx.activity:activity-compose”, version.ref = “androidx-activityCompose” }
- indicates that the androidx-activityCompose standard library version references the version number 1.9.0 defined by androidx-activityCompose.
- For example, androidx-activity-compose = { module = “androidx.activity:activity-compose”, version.ref = “androidx-activityCompose” }
- Plugin definitions
[plugins]
:
This section defines the plugins used in the project and their version information
Each plugin defines anid
and aversion.ref
whereid
is the plugin identifier
andversion.ref
also references the version number defined above
- For example, kotlinMultiplatform = { id = “org.jetbrains.kotlin.multiplatform”, version.ref = “kotlin” }
- indicates that the kotlinMultiplatform Plugin version references the version number
2.0.10-RC
defined by kotlin.
- Once the
.toml
file is configured
after clicking sync project
you can directly configure dependencies in your Gradle configuration
using thelibs.xxx.xx
notation
For example:
implementation(libs.androidx.activity.compose)
-
This configuration approach makes project dependency version management more centralized and unified, facilitating version upgrades and maintenance. Through the libs.versions.toml file, the project can clearly see all dependency version information, avoiding the confusion caused by defining version numbers in multiple places.
- You can also check out issues encountered when migrating to version catalogs with .toml for reference
build.gradle.kts(:composeApp)
is a Gradle build script file used to configure CMP
projects
It uses Kotlin DSL (Domain Specific Language) to define build configurations
This approach provides stronger type safety
(Null safety) and better IDE support
It mainly affects your app’s behavior during compilation
Since the build.gradle.kts
for CMP
projects is quite lengthy
I’ll explain it in parts
plugins
block: Used to import plugins
The imported plugins use what’s declared in thelib.version.toml
Like this:
plugins { alias(libs.plugins.multiplatform) alias(libs.plugins.compose.compiler) alias(libs.plugins.compose) alias(libs.plugins.android.application) alias(libs.plugins.buildConfig) alias(libs.plugins.kotlinx.serialization) }
kotlin
block:
This is where you place configuration items for the CMP
project
Such as configuration for shared files
(Strings, images, etc.)
JDK compilation settings
Or settings for different target platforms
Here’s a brief explanation:
-
androidTarget > compilerOptions > jvmTarget.set(JvmTarget.JVM_17)
Sets compilation to use JDK 17 -
cocoapods
: Importscocoapods
viaGradle
to use iOS frameworks -
listOf(iosX64(), iosArm64(), iosSimulatorArm64()).forEach { target -> ...
:
Configures iOS, for example usingcinterops
to bridge to iOS
Allowing CMP to use specified iOS frameworks -
sourceSets > androidMain.dependencies
andcommonMain.dependencies
, etc.:
Here you can specify dependencies for different platforms
For example: the androidMain block imports libraries used byandroid
The commonMain block imports libraries used byshared logic
You can even use iosMain to import libraries foriOS
However, the example below uses bridging
so it uses
listOf(iosX64(), iosArm64(), iosSimulatorArm64())
to import iOS frameworks
I’ll just cover this briefly for now
Later chapters will provide more detailed explanations about iOS bridging
- Below is a simple example of a
build.gradle.kts
file
kotlin {
androidTarget {
@OptIn(ExperimentalKotlinGradlePluginApi::class)
compilerOptions {
jvmTarget.set(JvmTarget.JVM_17)
}
}
cocoapods {
summary = "Bidapp ads for kotlin multiplatform"
version = "1.0"
homepage = "https://github.com/JetBrains/kotlin"
ios.deploymentTarget = "15.4"
podfile = project.file("../iosApp/Podfile")
name = "composeApp"
pod("Google-Mobile-Ads-SDK")
framework {
baseName = "composeApp"
linkerOpts.add("-lsqlite3")
isStatic = true
binaryOption("bundleId", "xxxx.edu")
}
}
listOf(iosX64(), iosArm64(), iosSimulatorArm64()).forEach { target ->
...
val frameworkPath = baseDir.resolve(targetArchitecture)
target.compilations.getByName("main") {
cinterops {
create("GoogleMobileAds") {
defFile(project.file("src/nativeInterop/cinterop/GoogleMobileAds.def"))
compilerOpts(
"-framework",
"GoogleMobileAds",
"-F$frameworkPath"
)
}
}
}
target.binaries.all {
linkerOpts(
"-framework",
"GoogleMobileAds",
"-F$frameworkPath"
)
}
}
sourceSets {
androidMain.dependencies {
implementation(compose.preview)
implementation(libs.androidx.activity.compose)
implementation(libs.google.ads)
}
commonMain.dependencies {
implementation(compose.runtime)
implementation(compose.foundation)
implementation(compose.material3)
implementation(compose.ui)
implementation(compose.components.resources)
implementation(compose.components.uiToolingPreview)
implementation(libs.koin.core)
implementation(libs.koin.compose)
implementation(libs.koin.compose.viewmodel)
implementation(libs.navigation.compose)
implementation(libs.datastore.core)
implementation(libs.ktor.serialization.kotlinx.json)
implementation(libs.androidx.room.runtime)
implementation(libs.sqlite.bundled)
implementation(libs.kotlinx.datetime)
}
}
}
- 可以看到
如果你想要在
共用邏輯導入material3
就直接在commomMain的block中導入即可
不過可能會有人好奇
為啥這邊是compose
.material而不是libs.xxxxxx
這是因為
當你導入KMM插件後
他有內建一下compose專案常用的library
讓你可以直接用
而不用
自己再去lib.version.toml中宣告
commonMain.dependencies {
implementation(compose.material3)
}