把 MVVMC 帶到 Android(三):走完之後,這條路值不值得?
Part 1 把專案搬進 Skip,Part 2 把六個功能逐一帶到 Android。技術細節寫完了。剩下一件事:這條路到底值不值得走?
🎯 我答應的事,做到了嗎?
Part 1 講過「核心承諾是 MVVMC 架構零破壞」。誠實打勾:
| 我答應的 | 結果 |
|---|---|
| M / VM / V / C 四層分界不能動 | ✅ |
doAction 單一進入點 | ✅ 兩個平台呼叫法一模一樣 |
Router enum 模式 | ✅ 兩個平台共用同一份 enum 定義 |
@Observable ViewModel 形狀 | ✅ |
| iOS 端 UIKit 啟動鏈不動 | ✅ |
代價:
- 約 130 行改動,散在 23 個檔案
- 多半是
publicmodifier、#if !SKIP包裝、八種小型寫法改寫 - Android 端的 C 層額外寫一份(同檔
#else分支) - 一條反向規則:iOS 用
[weak self],Android 用強參照
M / VM / V 共用同一份 Swift 原始碼;C 層寫兩份。架構意義上零破壞——但程式碼上不是「一字不改」,是「該長的肉長了、骨架沒動」。
💸 還有沒有技術債?
先講清楚一個前提:這個 MVVMC-Skip 是 demo 專案——六個功能、假資料、沒有真實 API、沒有複雜畫面。能盤點到的技術債本來就有限,正式專案的表面積會大得多。
就 demo 範圍而言,一筆,現在沒了:UserDetail 的 .task 在 Android 因為 NavigationStack 推進時被取消,當初我直接換成 .onAppear { Task { } }——但這份 View 是 iOS / Android 共用,iOS 也被一起換掉、失去結構化並行。而且這是整個專案的體質問題,不是 UserDetail 個案——任何 .task 在 Skip 上的生命週期都跟 iOS 不一樣。
正確做法:
| |
寫這篇時順手把專案裡所有
.task都用#if !SKIP包好、push 上去了。一視同仁:只要是 iOS / Android 共用的 View,.task一律走兩個分支。
🧭 學到什麼
1. 「自動跨平台」是行銷詞
「Swift 寫一份、Android 自動有」背後是 三道關卡:
- 轉譯:Swift 能不能變成 Kotlin?
- 編譯:那份 Kotlin 能不能編譯通過 AndroidX / SkipUI?
- 執行期:Compose 的重繪機制能不能跟
@ObservableViewModel 對得上?
三道形狀互不重疊,每一道都有自己的解法。
2. C 層注定要寫兩份
iOS 的 C 層是 MVVMC 原生的 HostController,Android 的 C 層是 SwiftUI struct + @State + AppRouter。職責一樣,實作不同。命名跟 init 簽名對齊,住在同一個 *HostController.swift 裡:
| |
3. 直觀的 Swift 寫法不一定 Skip 友善
巢狀的省略型別 enum(.success(...) 配 .failure(...))、case where、巢狀 case 解構——Swift 編譯器吃得下,Skip 看不穿。寫跨平台會養成新風格:少用省略型別、多寫顯式型別、巢狀解構拆兩層:
| |
4. ARC vs GC 的反向規則
iOS 用 [weak self] 是反射動作,Android 用 Kotlin GC 不需要 weak;習慣性寫 [weak] 反而會踩到「弱參照在跨 closure 鏈中提早被清空」的雷:
| |
iOS 用 weak、Android 用強參照——平常不會碰到,碰到會找 bug 找半天。
👤 適合誰?不適合誰?
適合:
- 已經用 MVVMC 的 iOS 專案,想伸手到 Android、但不想重寫架構
- 願意把 C 層寫兩份(換來 M / VM / V 三層共用)
- 願意忍受 Skip 的三道關卡,特別是執行期那道
不適合:
- 新專案 + 跨平台需求——直接純 SwiftUI 跨平台比較單純
- 還沒採用 MVVMC、覺得
HostController+AppRouter太重——那這套架構本身就不適合你 - 希望「一份程式碼一字不差跑兩個平台」
- 高互動 / 高效能需求(遊戲、影像處理)
MVVMC + Skip 是給「已經選了 MVVMC、又想去 Android」的人走的——窄,但實用。
📍 結尾
三篇文章證明的事就一句:MVVMC 這套 iOS 架構,透過 Skip 真的可以跨到 Android,而且不需要為了跨平台重寫架構。
系列對應的實驗 repo:MVVMC-Skip。完整的決策軌跡與驗證紀錄在該 repo 的
CLAUDE.mdMigration Log M0–M21。環境:Skip 1.9.3、Swift 6(swift-tools 6.1)、Xcode iOS Simulator (iPhone 17, iOS 17+)、Android Pixel 9 emulator、Skip Lite 模式(transpiled to Kotlin / Compose)。
本文使用 Claude 共同完成