iT邦幫忙

2025 iThome 鐵人賽

DAY 12
0
生成式 AI

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

Day 12: 開發者的困擾:Compose Desktop 不好除錯

  • 分享至 

  • xImage
  •  

昨天學會了用 ARS 做技術研究,今天馬上就派上用場了。

下午我正在開發 Grimo 的專案列表功能。

突然,應用程式崩潰了:

Exception in thread "AWT-EventQueue-0" java.lang.RuntimeException: 
    at androidx.compose.ui.platform.DesktopOwners_desktopKt$setContent$3.invoke
    at androidx.compose.ui.platform.DesktopOwners_desktopKt$setContent$3.invoke
    ... 47 more lines of stack trace ...
Caused by: java.lang.IllegalStateException
    at ProjectListViewModel.kt:45
    ... 23 more lines ...

看著這堆 stack trace,我整個人都不好了。

錯誤發生在哪?UI 層?ViewModel?還是資料層?IllegalStateException 到底是什麼狀態出問題?為什麼 iOS 開發者總說 KMP 的錯誤處理很糟糕?

Compose Desktop 除錯的痛點

痛點 1:Stack Trace 難以理解

Compose Desktop 的 stack trace 混合了:

  • Swing/AWT 的執行緒
  • Compose 的 recomposition
  • Kotlin Coroutines 的呼叫鏈
  • 你的業務邏輯

結果就是 100 行的 stack trace,只有 2 行是你的程式碼。

痛點 2:錯誤傳播不透明

// ViewModel
viewModelScope.launch {
    try {
        val project = repository.getProject(id)
        _state.value = Success(project)
    } catch (e: Exception) {
        // 這裡捕獲的是什麼?
        // SQLException? IOException? 還是其他?
        _state.value = Error(e.message ?: "Unknown error")
    }
}

當錯誤從資料層傳到 UI,原始的錯誤資訊早就丟失了。

痛點 3:iOS 互操作性災難

// iOS 端收到的錯誤
do {
    let project = try await repository.getProject(id: projectId)
} catch {
    // error 永遠是 NSError
    // 沒有類型資訊
    // 訊息可能是 null
    print("Error: \(error)")  // "Error: kotlin.Exception"
}

iOS 開發者:「你們的錯誤處理呢?」
我:「...」

請 ARS 出場

面對這些問題,我意識到需要系統性的解決方案。

是時候請出 ARS(Architecture Research Specialist)了。

我的研究請求

請以 ARS 角色研究 Kotlin Multiplatform 專案的錯誤處理最佳實踐,
特別關注:
1. Compose Desktop 的除錯體驗
2. iOS/Swift 互操作性
3. 2025 年的最新實踐
4. 實際專案案例

需求背景:
- 單人開發團隊
- Desktop 優先,未來支援 iOS/Android
- 需要清晰的錯誤追蹤

ARS 的研究過程

定義研究範圍 → 搜尋業界實踐 → 分析KMP生態 → 研究Swift Export → 評估解決方案 → 產出報告 → 實作建議

ARS 研究報告

經過深入研究,ARS 很快產出了這份報告:

研究報告:KMP 錯誤處理最佳實踐 (2025)

執行摘要

核心發現是傳統 try-catch 在 KMP 已過時。推薦方案是 Result Type + Sealed Class。關鍵技術包括 Kotlin 2.2.20 Swift Export。除錯效率會大幅提升。

問題分析

為什麼傳統方式失敗?

// ❌ 傳統方式的問題
class ProjectRepository {
    suspend fun getProject(id: String): Project {
        return database.getProject(id)  // 可能拋出 SQLException
    }
}

// 在 iOS 端變成
func getProject(id: String) async throws -> Project
// throws 只能拋出 NSError,丟失所有類型資訊

JVM 特定異常污染也是個問題。

// ❌ commonMain 不應該有 JVM 特定類型
catch (e: SQLException) {  // 編譯錯誤:iOS 沒有 SQLException
    // ...
}

解決方案:Result Type Pattern

核心設計如下:

// ✅ Sealed Class 定義錯誤類型
sealed interface AppError {
    val message: String
    val isRecoverable: Boolean
    
    data class Database(
        override val message: String,
        val operation: String? = null,
        override val isRecoverable: Boolean = true
    ) : AppError
    
    data class Network(
        override val message: String,
        val statusCode: Int? = null,
        override val isRecoverable: Boolean = true
    ) : AppError
    
    data class Validation(
        val field: String,
        val reason: String,
        override val message: String = "$field: $reason",
        override val isRecoverable: Boolean = false
    ) : AppError
}

// ✅ Result 包裝器
sealed class AppResult<out T, out E : AppError> {
    data class Success<T>(val value: T) : AppResult<T, Nothing>()
    data class Failure<E : AppError>(val error: E) : AppResult<Nothing, E>()
}

優雅的錯誤處理方式:

// Repository 層
class ProjectRepository {
    suspend fun getProject(id: String): AppResult<Project, AppError> {
        return AppResult.catching {
            database.getProject(id)
        }
    }
}

// ViewModel 層
class ProjectViewModel {
    fun loadProject(id: String) {
        viewModelScope.launch {
            repository.getProject(id).fold(
                onSuccess = { project ->
                    logger.debug { "載入專案成功: ${project.name}" }
                    _state.value = UiState.Success(project)
                },
                onFailure = { error ->
                    logger.error { "載入專案失敗: ${error.message}" }
                    when (error) {
                        is AppError.Network -> handleNetworkError(error)
                        is AppError.Database -> handleDatabaseError(error)
                        else -> showGenericError(error)
                    }
                }
            )
        }
    }
}

平台特定實作

expect/actual 機制:

// commonMain
expect fun mapThrowableToError(throwable: Throwable): AppError

// desktopMain
actual fun mapThrowableToError(throwable: Throwable): AppError {
    return when (throwable) {
        is SQLException -> {
            when {
                throwable.message?.contains("locked") == true -> 
                    AppError.Database.locked()
                throwable.message?.contains("no such table") == true ->
                    AppError.Database.tableNotFound(extractTableName(throwable))
                else -> 
                    AppError.Database(
                        message = throwable.message ?: "資料庫錯誤",
                        sqlState = throwable.sqlState
                    )
            }
        }
        is SocketTimeoutException -> 
            AppError.Network.timeout(throwable.message ?: "")
        is IOException -> 
            AppError.Network(message = "網路錯誤: ${throwable.message}")
        else -> 
            AppError.Unknown(message = throwable.message ?: "未知錯誤")
    }
}

案例研究

Cash App (Square) 有 50+ 開發者,採用 Result + Sealed Class,錯誤報告準確度大幅提升。

Expedia 有 20+ 開發者,採用 Arrow-kt Either,iOS 團隊滿意度大幅提升。

JetBrains Fleet 有 100+ 開發者,採用自定義 Result + Coroutines,除錯效率明顯提升。

實作建議

基於你的需求(單人團隊、Desktop 優先),建議:

  1. 立即實施

    • Result Type Pattern
    • Sealed Error Classes
    • 結構化日誌
  2. 工具配置

    // 協程異常處理
    val errorHandler = CoroutineExceptionHandler { _, error ->
        val appError = mapThrowableToError(error)
        logger.error { "協程異常: ${appError.message}" }
    }
    
  3. 除錯增強

    // 開發模式的詳細錯誤
    data class Unknown(
        override val message: String,
        val originalMessage: String? = null,
        val stackTrace: String? = if (DEBUG) throwable?.stackTraceToString() else null
    ) : AppError
    

實際應用成果

Before

// 錯誤處理一團糟
try {
    val project = repository.getProject(id)
    // ...
} catch (e: Exception) {
    println("Error: ${e.message}")  // "Error: null"
    // 然後呢?🤷
}

除錯流程很痛苦。看 stack trace、加 print statements、重現問題、猜測原因。每一步都很耗時。

After

// 清晰的錯誤處理
repository.getProject(id).fold(
    onSuccess = { project ->
        logger.info { "專案載入成功: ${project.name}" }
    },
    onFailure = { error ->
        when (error) {
            is AppError.Database -> {
                logger.error { "資料庫錯誤: operation=${error.operation}" }
                if (error.isRecoverable) {
                    retryWithBackoff()
                } else {
                    showDatabaseError(error)
                }
            }
            is AppError.Network -> {
                logger.warn { "網路錯誤: status=${error.statusCode}" }
                loadFromCache()
            }
            else -> handleUnexpectedError(error)
        }
    }
)

現在的除錯流程簡單多了。看錯誤類型、檢查錯誤詳情、定位問題、修復。每一步都很清楚。

實際收益

除錯體驗改善了。錯誤定位變快、修復時間縮短、錯誤重現率降低。

程式碼品質也提升了:

// Before: 大量混亂的 try-catch
// After: 清晰的 Result 處理

// Before: 大量 catch (e: Exception)
// After: 無通用 Exception 捕獲

// Before: 錯誤無法追蹤
// After: 所有錯誤都有類型

iOS 準備就緒了:

// Swift 端的體驗(使用 Kotlin 2.2.20 Swift Export)
let result = await repository.getProject(id: projectId)
switch result {
case .success(let project):
    print("載入成功: \(project.name)")
case .failure(let error):
    switch error {
    case let dbError as AppError.Database:
        print("資料庫錯誤: \(dbError.message)")
    case let netError as AppError.Network:
        print("網路錯誤: 狀態碼 \(netError.statusCode ?? 0)")
    default:
        print("其他錯誤")
    }
}

iOS 開發者:「終於有個像樣的錯誤處理了!」

ARS 帶來的洞察

不只是解決問題

ARS 不只幫我解決了錯誤處理問題,還帶來了架構層面的思考:

錯誤是領域知識

// 錯誤不是「異常」,而是業務邏輯的一部分
sealed interface ProjectError : AppError {
    data class NotFound(val projectId: String) : ProjectError
    data class NoPermission(val userId: String) : ProjectError
    data class Archived(val archivedDate: Long) : ProjectError
}

可復原性設計

interface AppError {
    val isRecoverable: Boolean
    val retryStrategy: RetryStrategy?
}

// 自動重試機制
suspend fun <T> withRetry(
    action: suspend () -> AppResult<T, AppError>
): AppResult<T, AppError> {
    var lastError: AppError? = null
    repeat(3) { attempt ->
        action().fold(
            onSuccess = { return AppResult.success(it) },
            onFailure = { error ->
                if (!error.isRecoverable) return AppResult.failure(error)
                lastError = error
                delay(exponentialBackoff(attempt))
            }
        )
    }
    return AppResult.failure(lastError!!)
}

錯誤觀察性

// 錯誤追蹤系統
class ErrorTracker {
    fun track(error: AppError) {
        analytics.track("error_occurred", mapOf(
            "type" to error::class.simpleName,
            "recoverable" to error.isRecoverable,
            "message" to error.message
        ))
        
        if (!error.isRecoverable) {
            crashlytics.recordException(
                ErrorTrackingException(error)
            )
        }
    }
}

ARS 的研究方法論價值

這次研究展現了 ARS 的核心價值。

全面性,不只看技術,還看團隊案例。時效性,關注 2025 最新實踐(Swift Export)。實用性,提供可執行的程式碼範例。深度,從問題根源到解決方案。

學到的經驗

預設錯誤處理架構

不要等到出問題才想錯誤處理:

// 專案初期就定義好
project-template/
├── core/
│   ├── error/
│   │   ├── AppError.kt
│   │   ├── AppResult.kt
│   │   └── ErrorHandler.kt

錯誤處理也需要測試

@Test
fun `當資料庫鎖定時應該回傳可復原錯誤`() {
    // Given
    every { database.query() } throws SQLException("database is locked")
    
    // When
    val result = repository.getData()
    
    // Then
    assertTrue(result is AppResult.Failure)
    val error = result.error
    assertTrue(error is AppError.Database)
    assertTrue(error.isRecoverable)
}

文件化錯誤場景

/**
 * 取得專案資料
 * 
 * @return 可能的錯誤:
 * - [AppError.Database.tableNotFound] - 資料表不存在
 * - [AppError.Network.timeout] - 同步逾時
 * - [AppError.Validation] - ID 格式錯誤
 */
suspend fun getProject(id: String): AppResult<Project, AppError>

實戰心得:ARS 讓研究變得高效

時間對比

不用 ARS 的話,要 Google 搜尋、看大量 Medium 文章、看 Stack Overflow、試錯實作。花很多時間,還不確定是否最佳實踐。

使用 ARS 就不一樣了。定義研究需求、ARS 研究、評估報告、實作。時間大幅縮短,而且有完整的最佳實踐支撐。

品質差異

ARS 的研究報告包含 3 個實際公司案例、完整的程式碼範例、優缺點分析、2025 最新技術、實作步驟指南。

這是單純 Google 搜尋無法達到的深度。

專業 AI 角色的必要性

今天的經驗再次證明,專業的問題需要專業的角色。

ARS 不只是一個「會搜尋的 AI」。它懂架構研究方法論、知道去哪找資料、會評估方案優劣、能給出實作建議。

這次的錯誤處理改造,讓我的除錯效率大幅提升。
更重要的是,建立了一套可持續的錯誤處理架構。

給你的建議

定義你的痛點,什麼問題最常困擾你?設計專門角色,為這個問題設計 AI 研究員。系統性解決,不要頭痛醫頭,要找根本方案。累積最佳實踐,把研究成果固化成架構。

記住,好的架構是研究出來的,不是猜出來的。

今日金句

「錯誤不是異常,是系統設計的一部分。」

關於作者:Sam,一人公司創辦人。正在打造 Grimo,一個智能任務管理和分配平台。

專案連結GitHub - grimostudio


上一篇
Day 11: 為什麼你需要一個 AI 架構研究專家(ARS)
下一篇
Day 13: 開發者的資料庫魔法:類型安全的 SQLDelight 存取之道
系列文
30 天一人公司的 AI 開發實戰14
圖片
  熱門推薦
圖片
{{ item.channelVendor }} | {{ item.webinarstarted }} |
{{ formatDate(item.duration) }}
直播中

尚未有邦友留言

立即登入留言