iT邦幫忙

2026 iThome 鐵人賽

DAY 3
0
自我挑戰組

愛安豬系列 第 3

# meow meow

  • 分享至 

  • xImage
  •  

Day 3:Source Set - commonMain、androidMain、iosMain

今天只做:src/ 底下幾個資料夾的關係弄清楚。

Android 的角度

Android developer 有在用類似的東西:src/mainsrc/debugsrc/release、還有 product flavor 的 src/freesrc/paid。這些都是 source set,AGP 依 build variant 把它們合併編譯。

KMP 的 source set 概念一樣,但切分的維度不是 build type,而是 target。而且合併的方式不是「疊起來」,而是「有方向的依賴」。

1. 專案裡到底有幾個 source set

目錄裡看得到的只有三個:

commonMain
androidMain
iosMain

但實際上 Gradle 建出來的比這多。跑一下:

./gradlew :composeApp:tasks --all | grep -i compile

or 在 build.gradle.kts 裡加一段 debug:

kotlin {
    sourceSets.all {
        println("sourceSet: $name  dependsOn: ${dependsOn.map { it.name }}")
    }
}

sync 一次,Build output 裡會看到類似:

sourceSet: commonMain            dependsOn: []
sourceSet: androidMain           dependsOn: [commonMain]
sourceSet: iosMain               dependsOn: [commonMain]
sourceSet: iosX64Main            dependsOn: [iosMain]
sourceSet: iosArm64Main          dependsOn: [iosMain]
sourceSet: iosSimulatorArm64Main dependsOn: [iosMain]
(以及對應的 *Test)

所以真正的圖是這樣:

                commonMain
               /          \
        androidMain      iosMain
                        /   |    \
                 iosX64  iosArm64  iosSimulatorArm64

iosMain 不對應任何一個 compile target,它是一個 intermediate source set——三個 iOS target 的公因數。這解釋了昨天的疑問:為什麼 iosMain 一份 code 可以同時服務 simulator 和實機。

這張圖是 Kotlin Gradle plugin 的 default hierarchy template 自動建的。只要宣告了 iosX64()iosArm64()iosSimulatorArm64() 三個 target,iosMain 就會自動出現,不需要手寫 dependsOn

2. dependsOn 的方向決定了「看得見什麼」

規則只有一條:

一個 source set 可以看見它 dependsOn 的所有 source set 的 code;反過來不行。

具體來說:

在這裡寫 code 看得見 看不見
commonMain Kotlin stdlib、multiplatform library 的 common API Android SDK、Foundation、java.、platform.
androidMain commonMain 全部 + Android SDK + JVM iosMain
iosMain commonMain 全部 + Kotlin/Native + Foundation/UIKit androidMain
iosArm64Main iosMain 全部 + arm64 專屬 其他 iOS target

這張表回答了 Day 1 那個問題的一半:commonMain 看不見 java.*,不是因為誰把它擋掉,而是 commonMain 編譯時根本沒有 JVM 這個東西存在。 它會被編成 JVM bytecode(給 Android)也會被編成 native binary(給 iOS),所以只能用兩邊都有的 API。
Question: 兩邊都有的 API 有哪些?

3. 用 compiler 驗證

昨天訂的規則,今天第一次正式執行。在 commonMain 隨便建一個檔案:

// commonMain/kotlin/Probe.kt
fun now(): Long = System.currentTimeMillis()

IDE 標示紅線:Unresolved reference: System

改成:

import kotlin.time.Clock  // Kotlin 2.x 的 multiplatform Clock

fun now(): Long = Clock.System.now().toEpochMilliseconds()

紅線消失。再把它搬去 androidMain,第一版 System.currentTimeMillis() 就能過。

再試一個更有意思的:

// iosMain/kotlin/Probe.ios.kt
import platform.Foundation.NSDate

fun iosNow() = NSDate()

這在 iosMain 可以編譯,證明 iosMain 看得見 Foundation。搬去 commonMain 就炸。

這種「寫一行、看紅線、搬資料夾」的動作,我建議每次遇到不確定的 API 都做一次。

4. Test 也是同一套結構

commonTest  → 用 kotlin.test,Android 和 iOS 都會跑
androidUnitTest → JVM 上跑,可以用 JUnit / Robolectric
iosTest     → Kotlin/Native 上跑,simulator 執行

一個 commonTest 裡的 test 會被編兩次、跑兩次。這意味著寫在 commonMain 的邏輯,一個 test 就同時驗證了兩個平台——這是 Phase 5 的重點,今天先知道結構就好。

./gradlew :composeApp:testDebugUnitTest        # Android
./gradlew :composeApp:iosSimulatorArm64Test    # iOS

5. 今天問 AI 什麼

這次我故意問一個有陷阱的問題,看它會不會踩:

In my KMP project I have commonMain, androidMain and iosMain.
I want a `Platform` object that reports the OS name and version.
Where should I put it, and should I use expect/actual or an interface?
Show both options and explain the trade-off. Don't pick one for me.

它給了兩個版本。expect/actual 版本正確,interface 版本也正確,但它在 trade-off 那段說「expect/actual 是官方推薦」——這句我沒在官方文件找到。官方文件的說法是兩者各有適用場景,interface 更適合需要 mock 或 DI 的情況。

這是一種常見的 AI 錯誤模式:把「常見寫法」說成「官方推薦」。 記進 Phase 5 的清單。

But 什麼時候用哪一個。今天只要知道:兩者的產物都是「commonMain 宣告、platform source set 實作」,本質上都在利用 dependsOn 的方向。

6. Commit

今天只有 probe 用的檔案,不留在 repo 裡。刪掉之後 commit build.gradle.kts 那段 debug println(之後會用到):

git commit -am "chore: print source set hierarchy on sync (AI: none)"

Day 3 完成條件

  • [x] 印出實際的 source set 圖,確認 iosMain 是 intermediate source set
  • [x] 理解 dependsOn 方向 = 可見性方向
  • [x] 用 compiler 驗證 commonMain 看不見 System / Foundation
  • [x] 知道 commonTest 會在兩個平台各跑一次
  • [x] 抓到 AI 一個「常見寫法 ≠ 官方推薦」的誤導

明天

Day 4,處理 Gradle 本身:version catalog 怎麼組織 KMP 的依賴、commonMain.dependenciesandroidMain.dependencies 的差別、以及怎麼判斷一個第三方 library 到底支不支援 KMP(提示:看它有沒有發 -iosarm64 的 artifact)。之後 Phase 2 就可以放心加 library 了。


上一篇
# meow mew
下一篇
# oops
系列文
愛安豬4
圖片
  熱門推薦
圖片
{{ item.channelVendor }} | {{ item.webinarstarted }} |
{{ formatDate(item.duration) }}
直播中

尚未有邦友留言

立即登入留言