iT邦幫忙

2025 iThome 鐵人賽

DAY 29
0
生成式 AI

30 天一人公司的 AI 開發實戰系列 第 29

Day 29: 架構師的擴展藍圖:從 Desktop First 到 Android-iOS

  • 分享至 

  • xImage
  •  

前言:逃不過的現實

專案開發到第四週,我很清楚地意識到一件事:

即使 Desktop 版做得再好,沒有手機版就是不完整。

因為大部分任務管理發生在移動中。在會議室、在咖啡廳、在通勤路上。

所以從一開始,我就把 Grimo 設計成跨平台架構。不是為了炫技,而是為了生存。

今天分享 KMP 跨平台擴展的實戰經驗。

為什麼選擇 Desktop First?

開發效率考量

Desktop 開發週期最短。不用處理 App Store 審核。除錯工具最完整。UI 迭代最快速。

一個人的資源有限,先做最容易成功的。

市場策略

開發者主要在桌面工作。先滿足核心用戶。收集回饋後再擴展。降低試錯成本。

但架構要從第一天就考慮跨平台。

KMP 的跨平台哲學

共享什麼?

// shared module - 90% 的商業邏輯
expect class PlatformInfo {
    val name: String
    val version: String
}

// 核心業務邏輯完全共享
class ProjectRepository {
    suspend fun getProject(id: String): Project {
        // 資料庫操作、網路請求、商業邏輯
        // 在所有平台上都一樣
    }
}

// UI 狀態管理也共享
class ProjectViewModel {
    val uiState: StateFlow<ProjectUiState>
    // MVI 模式在所有平台通用
}

平台特定實作

// desktopMain
actual class PlatformInfo {
    actual val name = "Desktop"
    actual val version = System.getProperty("os.version")
}

// androidMain  
actual class PlatformInfo {
    actual val name = "Android"
    actual val version = Build.VERSION.RELEASE
}

// iosMain
actual class PlatformInfo {
    actual val name = "iOS"
    actual val version = UIDevice.currentDevice.systemVersion
}

從 Desktop 到 Mobile 的關鍵決策

1. 資料同步策略

Local-First 是核心

每個裝置都有完整的本地資料庫。離線優先,線上同步。用戶永遠不會因為網路問題無法工作。

// 通用的同步介面
interface SyncStrategy {
    suspend fun sync(): Result<SyncStatus>
}

// Desktop:檔案系統同步
class DesktopSyncStrategy : SyncStrategy {
    override suspend fun sync() = 
        fileSystemSync() // 透過 Dropbox/iCloud
}

// Mobile:API 同步
class MobileSyncStrategy : SyncStrategy {
    override suspend fun sync() = 
        apiSync() // 透過後端 API
}

2. UI 架構決策

Compose Multiplatform vs Native UI

這是最難的決定。

Compose Multiplatform 的優點:

  • 一套程式碼,三個平台
  • UI 邏輯 100% 共享
  • 開發速度快

但我選擇了 Native UI:

  • iOS 用 SwiftUI
  • Android 用 Compose
  • Desktop 繼續用 Compose Multiplatform

為什麼?

因為用戶體驗。

iOS 用戶期待 iOS 的操作感。Android 用戶期待 Material Design。強行統一反而造成困擾。

3. 共享 ViewModel 模式

// shared module
class ProjectViewModel {
    private val _state = MutableStateFlow(ProjectState())
    val state: StateFlow<ProjectState> = _state
    
    fun loadProject(id: String) {
        viewModelScope.launch {
            repository.getProject(id)
                .onSuccess { _state.update { it.copy(project = project) } }
                .onFailure { _state.update { it.copy(error = error) } }
        }
    }
}

iOS 端使用:

// iOS SwiftUI
struct ProjectView: View {
    @StateObject private var viewModel = ProjectViewModel()
    
    var body: some View {
        // 直接使用 KMP 的 ViewModel
        if let project = viewModel.state.project {
            Text(project.name)
        }
    }
}

Android 端使用:

// Android Compose
@Composable
fun ProjectScreen(viewModel: ProjectViewModel) {
    val state by viewModel.state.collectAsState()
    
    state.project?.let { project ->
        Text(project.name)
    }
}

神奇的是,商業邏輯完全一樣!

實戰:三週完成 iOS 版

Week 1: 基礎建設

設定 iOS target。建立 SwiftUI 專案。連接 KMP framework。測試基本功能。

最難的部分?Xcode 和 Gradle 的整合。

解決方案:

// build.gradle.kts
kotlin {
    ios {
        binaries {
            framework {
                baseName = "GrimoShared"
                // 關鍵:export 需要的類別
                export(project(":shared"))
            }
        }
    }
}

Week 2: UI 開發

SwiftUI 其實不難學。特別是有 Compose 經驗後。

概念都是響應式 UI:

// SwiftUI - 很像 Compose
struct TaskListView: View {
    @State private var tasks: [Task] = []
    
    var body: some View {
        List(tasks) { task in
            TaskRow(task: task)
        }
    }
}

重點是保持 Native 體驗。不要強行移植 Desktop 的操作邏輯。

Week 3: 整合與測試

這週最累。

處理 iOS 特定功能:推送通知、Face ID、深色模式、手勢操作。

但因為核心邏輯都在 KMP,真正要寫的程式碼很少。

Android 版本的意外簡單

有了 iOS 經驗,Android 版只花了一週。

為什麼這麼快?

  1. Compose 直接用 - Desktop 的 UI 元件稍微調整就能用
  2. 權限處理簡單 - 比 iOS 直觀
  3. 測試更容易 - 模擬器比 iOS 快
// 90% 的程式碼直接複用
@Composable
fun ProjectScreenAndroid() {
    // 幾乎和 Desktop 版一樣
    ProjectScreen(
        modifier = Modifier.fillMaxSize(),
        // 只需要調整一些平台特定行為
        onBackPressed = { activity.onBackPressed() }
    )
}

平台特色功能

iOS 獨有

// Widget 支援
struct GrimoWidget: Widget {
    var body: some WidgetConfiguration {
        StaticConfiguration(kind: "TaskWidget") { entry in
            TaskWidgetView(entry: entry)
        }
    }
}

// Shortcuts
struct GrimoShortcuts {
    static func setupShortcuts() {
        let shortcut = UIApplicationShortcutItem(
            type: "new_task",
            localizedTitle: "新增任務"
        )
    }
}

Android 獨有

// Material You 動態主題
@Composable
fun GrimoTheme(
    dynamicColor: Boolean = true,
    content: @Composable () -> Unit
) {
    val colorScheme = when {
        dynamicColor && Build.VERSION.SDK_INT >= 31 -> {
            dynamicDarkColorScheme(LocalContext.current)
        }
        else -> DefaultColorScheme
    }
}

// 快速設定磚
class QuickTaskTileService : TileService() {
    override fun onClick() {
        // 快速新增任務
    }
}

經驗教訓

做對的事

  1. 先驗證需求 - Desktop 版證明產品價值後再擴展
  2. 架構先行 - 從第一天就設計成跨平台
  3. 尊重平台 - 不要強求 UI 一致性

踩過的坑

  1. iOS 記憶體限制 - Extension 只有 120MB
  2. Android 版本碎片 - 要支援到 API 24
  3. 同步衝突 - 多裝置編輯同一任務

意外收穫

KMP 讓跨平台開發變得可行。對一人公司來說,這是遊戲規則改變者。

寫一次商業邏輯,三個平台都能用。維護成本大幅降低。更新功能時,只需要改一個地方。

未來展望

Phase 1: 完善行動版(目前)

  • iOS TestFlight 測試
  • Android Beta 計畫
  • 收集用戶回饋

Phase 2: 平台整合

  • Apple Watch 應用
  • Android Wear 支援
  • 平板優化

Phase 3: 新平台探索

  • Web 版本(KMP/JS)
  • Windows 原生版
  • Linux 支援

給一人公司的建議

技術選型

如果你要做跨平台應用:

資源有限:選 Flutter 或 React Native,快速出貨。

長期維護:選 KMP,程式碼品質高,維護成本低。

用戶體驗優先:Native 開發,但成本最高。

開發順序

我的建議:

  1. Web First - 如果你的應用適合網頁
  2. Mobile First - 如果核心場景在手機
  3. Desktop First - 如果目標是專業用戶

選擇最能快速驗證的平台。

架構設計

無論選什麼技術,記住:

分離關注點 - UI 和業務邏輯要分開。統一資料流 - 所有平台用同樣的狀態管理。漸進式擴展 - 不要一開始就做所有平台。

結語

從 Desktop 到 iOS 再到 Android,Grimo 的跨平台之旅證明了:

一人公司也能開發多平台應用。

關鍵不是技術有多強,而是架構設計是否合理。

KMP 給了我們一個選擇:用較少的資源,達到較好的結果。

當你的 Desktop 應用運行穩定,當用戶開始詢問「有手機版嗎?」,當你看到 KMP 編譯出 iOS framework...

你會發現,跨平台不再是夢想,而是週末就能實現的計劃。

記住:平台是手段,解決問題才是目的。


上一篇
Day 28: 策略長的商業思考:行銷策略與收入方式研究
系列文
30 天一人公司的 AI 開發實戰29
圖片
  熱門推薦
圖片
{{ item.channelVendor }} | {{ item.webinarstarted }} |
{{ formatDate(item.duration) }}
直播中

尚未有邦友留言

立即登入留言