以「上傳 Android ID」為例,聊聊回調的新寫法

一、背景

在 Android 項目中,我們常常寫出類似這樣的接口:

fun sendAndroidIdToServer(uuid: String, onSuc: (Boolean) -> Unit)

用來執行一個網絡請求,並在成功後通過回調通知調用方。但這種寫法有個問題:

每次都要傳一個回調函數,哪怕只是打印個日誌,也得寫 {}

於是,我們就可以用 Kotlin 高階函數的默認參數 來讓代碼更優雅。

二、高階函數是什麼?

在 Kotlin 中,高階函數就是“參數或返回值是函數的函數”。
比如:

fun repeatTask(times: Int, action: () -> Unit) {
    repeat(times) { action() }
}

它允許你把函數當參數傳遞,這正是回調函數的基礎能力。

三、讓回調可選:默認參數 + 空實現

我們可以這樣改寫:

fun sendAndroidIdToServer(
    uuid: String,
    onSuc: (Boolean) -> Unit = {} // 默認空實現
) {
    // ...執行網絡邏輯
    onSuc(true)
}

這樣調用就靈活了:

sendAndroidIdToServer(deviceId)                 // 不關心結果
sendAndroidIdToServer(deviceId) { ok -> ... }   // 需要時再寫回調

✅ 好處:調用更乾淨,不用每次都寫 {} 


四、帶默認行為:自帶日誌的回調

進一步優化:即使不傳 onSuc,也能自動打印日誌。

private const val TAG = "MainViewModel"
fun sendAndroidIdToServer(
    uuid: String,
    onSuc: (Boolean) -> Unit = { success ->
        Log.d(TAG, "sendAndroidIdToServer result = $success")
    }
) {
    launchFlow(errorCall = object : IApiErrorCallback {
        override fun onError(code: Int?, error: String?) {
            Log.e(TAG, "上傳失敗: $error")
            onSuc(false)
        }
        override fun onLoginFail(code: Int?, error: String?) {
            Log.e(TAG, "登錄失敗: $error")
            onSuc(false)
        }
    }, requestCall = {
        homeRepository.sendAndroidId(uuid)
    }, showLoading = { isLoading ->
        _isLoading.value = isLoading
    }) { data ->
        Log.d(TAG, "上傳標識id成功: $data")
        onSuc(true)
    }
}

這樣即使你調用:

sendAndroidIdToServer(deviceId)

也會自動輸出:

sendAndroidIdToServer result = true

五、代碼可讀性提升技巧

✅ 1. 用 typealias 讓語義更清晰

typealias OnResult = (Boolean) -> Unit
fun sendAndroidIdToServer(uuid: String, onSuc: OnResult = {}) { ... }

比 (Boolean) -> Unit 更易懂。


✅ 2. 用 Sealed/Result 擴展可讀性

當結果不只是成功/失敗,可以定義:

sealed interface UploadResult {
    data object Ok : UploadResult
    data class Fail(val code: Int?, val msg: String?) : UploadResult
}
typealias OnUpload = (UploadResult) -> Unit

這樣更容易拓展成多狀態結構。


✅ 3. 支持雙回調形式(命令式寫法)

sealed interface UploadResult {
    data object Ok : UploadResult
    data class Fail(val code: Int?, val msg: String?) : UploadResult
}
typealias OnUpload = (UploadResult) -> Unit

適合語義明確的命令型操作。


✅ 4. 可空 vs 默認回調

兩種寫法的對比:

寫法

調用

優缺點

onSuc: ((Boolean) -> Unit)? = null

onSuc?.invoke(true)

需判空;語義明確

onSuc: (Boolean) -> Unit = {}

onSuc(true)

無需判空;更簡潔 ✅


六、進階:結合協程更優雅

用 suspend + Result 可以讓結構更清晰:

sealed interface UploadResult {
    data object Ok : UploadResult
    data class Fail(val code: Int?, val msg: String?) : UploadResult
}
typealias OnUpload = (UploadResult) -> Unit

這樣錯誤用異常控制,不需要多層回調。


七、常見坑與最佳實踐

問題

建議

忘記調用回調

保證每個分支都 onSuc()

多線程

明確回調在哪個線程(UI/Main)

默認回調副作用

默認回調只做日誌或統計,不改狀態

拋異常

用 try/catch 包回調執行

調試麻煩

默認回調打印詳細日誌


八、總結一句話

Kotlin 高階函數 + 默認參數 = 更優雅的回調設計

讓你的 API:

  • ✔ 可選回調
  • ✔ 默認日誌行為
  • ✔ 可讀可測
  • ✔ 不傳也安全

示例總結:

typealias OnResult = (Boolean) -> Unit
fun sendAndroidIdToServer(
    uuid: String,
    onSuc: OnResult = { success -> Log.d("MainVM", "result=$success") }
) { /* ... */ }

調用時:

sendAndroidIdToServer(deviceId)            // 自動打印日誌
sendAndroidIdToServer(deviceId) { ok -> … } // 需要時寫自定義回調

注意: 如果用下一種方式,默認回調被覆蓋了,不會執行。

所以看不到 Log.d("MainVM", "result=$success") 這個日誌。

 最後一句

Kotlin 的高階函數,不僅讓回調更優雅,
也讓「不用回調」變成了一種安全的設計習慣。