以「上傳 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 默認回調
兩種寫法的對比:
|
寫法
|
調用
|
優缺點
|
|
|
|
需判空;語義明確
|
|
|
|
無需判空;更簡潔 ✅
|
六、進階:結合協程更優雅
用 suspend + Result 可以讓結構更清晰:
sealed interface UploadResult {
data object Ok : UploadResult
data class Fail(val code: Int?, val msg: String?) : UploadResult
}
typealias OnUpload = (UploadResult) -> Unit
這樣錯誤用異常控制,不需要多層回調。
七、常見坑與最佳實踐
|
問題
|
建議
|
|
忘記調用回調
|
保證每個分支都 |
|
多線程
|
明確回調在哪個線程(UI/Main)
|
|
默認回調副作用
|
默認回調只做日誌或統計,不改狀態
|
|
拋異常
|
用 |
|
調試麻煩
|
默認回調打印詳細日誌
|
八、總結一句話
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 的高階函數,不僅讓回調更優雅,
也讓「不用回調」變成了一種安全的設計習慣。