今天目標:
commonMain裡的App()同時出現在 Android emulator 和 iOS simulator 上。
沒用 Ktor、沒用有 Room、沒用 Repository。
先讓 toolchain 跑過。
使用 Day 1 prompt,AI 給的答案大致分三類:說對的、說得太籠統的、以及版本過時的。
對的:Context、SharedPreferences、Log、java.io.File 在 commonMain 全部不存在。
籠統的:它說「用 expect/actual 解決」,但沒講什麼時候該用 expect/actual、什麼時候該用 interface。
過時的:它推薦的 library 版本和 Gradle 寫法,跟今天 wizard 產出來的專案已經不一樣。
Lesson:問 AI 「怎麼設定專案」是最容易被誤導的問題,因為 KMP 的 Gradle DSL 每半年就變一次。 專案骨架要用官方工具產生,AI 只負責解釋產出來的東西。AI 常常搞錯.
KMP 官方提供 web wizard(kmp.jetbrains.com),Android Studio / IntelliJ 裝 Kotlin Multiplatform plugin 後也有同樣的 New Project 精靈。
選項:
KMPReader
手刻 build.gradle.kts 在 KMP 很麻煩。Kotlin plugin、Compose compiler plugin、AGP 三者的版本組合有相容矩陣,wizard 產出的組合是 JetBrains 測過的;自己配很容易踩到「某個 alpha 的 CMP 不支援某版 AGP」這種跟學習無關的坑。
寫這篇時 wizard 給我的版本是 Kotlin 2.4.x、Compose Multiplatform 1.11.x、AGP 9.x。你看到這篇時大概又不一樣了,這正是我不把版本號寫進正文的原因——請以 gradle/libs.versions.toml 為準。
KMPReader/
├── composeApp/
│ ├── build.gradle.kts
│ └── src/
│ ├── commonMain/kotlin/ ← App() 在這裡
│ ├── androidMain/kotlin/ ← MainActivity
│ └── iosMain/kotlin/ ← MainViewController
├── iosApp/ ← Xcode project,Swift 只有幾行
├── gradle/libs.versions.toml
├── build.gradle.kts
└── settings.gradle.kts
第一次看到有幾個地方跟 Android 專案直覺不同:
只有一個 module 叫 composeApp,沒有 app。
它同時是 Android application 和 iOS framework 的來源。
iOS 那邊是 iosApp 這個 Xcode project 去 link 這個 module 編出來的 framework。
androidMain 裡的 MainActivity 只做一件事:
class MainActivity : ComponentActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContent { App() }
}
}
iosMain 裡的對應物是一個 function,不是 class:
fun MainViewController() = ComposeUIViewController { App() }
Swift 端 iosApp/iosApp/ContentView.swift 把它包成 UIViewControllerRepresentable。整個 iOS app 的 Swift code 加起來不到 30 行。
App() 本身在 commonMain:
@Composable
fun App() {
MaterialTheme {
// wizard 給的 demo UI
}
}
寫 Android 的人看到這個結構第一個反應:所以 Activity 只是個 host?
對。這是 KMP 專案的基本姿勢
——平台 entry point 薄到只剩一行,所有東西往 commonMain 推。
選 composeApp configuration,emulator 跑起來,看到 wizard 的 demo 畫面。
值得注意的是 composeApp/build.gradle.kts 裡 android { } block 和以前的 app module 幾乎一樣,namespace、compileSdk、defaultConfig 都在。KMP 沒有換掉 Android 的 build 流程,只是把它變成眾多 target 之一。
我的 Mac 之前沒有做 iOS 開發,所以這段花的時間比 Android 多很多。步驟:
kdoctor(brew install kdoctor),它會檢查 Xcode、CocoaPods、JDK、Kotlin plugin 是否就位。先跑這個再開 IDE,可以省掉一半的莫名錯誤。
iosApp,target 選一台 simulator。第一次 iOS build 會很慢,因為 Kotlin/Native 要把 commonMain 編成 native framework,還要下載 Kotlin/Native toolchain。我這裡等了幾分鐘。之後 incremental build 會快很多。
跑起來之後,同一個 demo 畫面出現在 iPhone simulator 上。Android 那邊的 Material 元件、字型、ripple,在 iOS 上長得一模一樣——這既是 CMP 的賣點,也是之後 Phase 4 要處理的問題:iOS 使用者不一定想看到 Material。
專案已經跑起來了,開始問 AI 。我把 composeApp/build.gradle.kts 整段貼給它:
This is the build.gradle.kts generated by the KMP wizard for an Android + iOS
Compose Multiplatform app.
Explain it block by block. In particular:
- why are there three iOS targets (iosX64, iosArm64, iosSimulatorArm64)?
- what does `binaries.framework { isStatic = true }` do and when would I set it false?
- which dependencies are in commonMain vs androidMain, and why?
Do not suggest changes yet.
三個 iOS target 的解釋它講得不錯:x64 是 Intel Mac simulator、simulatorArm64 是 Apple Silicon simulator、arm64 是實機。static framework 的部分它說得對但不完整,我回頭查了官方文件補上 dynamic framework 在 CocoaPods 整合時的差異。
這就是昨天訂的規則:AI 解釋、我對照官方文件、compiler 決定誰是對的。
git add .
git commit -m "feat: KMP Reader skeleton from wizard, runs on Android + iOS (AI: none)"
Day 3 要把今天「看懂」的東西拆開來講:KMP 的 source set 是怎麼組織的、commonMain / androidMain / iosMain 之間的依賴方向、為什麼 iosMain 可以同時服務三個 iOS target。這是後面所有「這個東西屬於誰」判斷的基礎。