以「上传 Android ID」为例,聊聊回调的新写法
一、背景
在 Android 项目中,我们常常写出类似这样的接口:
Kotlin
fun sendAndroidIdToServer(uuid: String, onSuc: (Boolean) -> Unit)
用来执行一个网络请求,并在成功后通过回调通知调用方。但这种写法有个问题:
每次都要传一个回调函数 ,哪怕只是打印个日志,也得写
{}。
于是,我们就可以用 Kotlin 高阶函数的默认参数 来让代码更优雅。
二、高阶函数是什么?
在 Kotlin 中,高阶函数 就是"参数或返回值是函数的函数"。
比如:
Kotlin
fun repeatTask(times: Int, action: () -> Unit) {
repeat(times) { action() }
}
它允许你把函数当参数传递,这正是回调函数的基础能力。
三、让回调可选:默认参数 + 空实现
我们可以这样改写:
Kotlin
fun sendAndroidIdToServer(
uuid: String,
onSuc: (Boolean) -> Unit = {} // 默认空实现
) {
// ...执行网络逻辑
onSuc(true)
}
这样调用就灵活了:
Kotlin
sendAndroidIdToServer(deviceId) // 不关心结果
sendAndroidIdToServer(deviceId) { ok -> ... } // 需要时再写回调
✅ 好处 :调用更干净,不用每次都写 {} 。
四、带默认行为:自带日志的回调
进一步优化:即使不传 onSuc,也能自动打印日志。
Kotlin
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 让语义更清晰
Kotlin
typealias OnResult = (Boolean) -> Unit
fun sendAndroidIdToServer(uuid: String, onSuc: OnResult = {}) { ... }
比 (Boolean) -> Unit 更易懂。
✅ 2. 用 Sealed/Result 扩展可读性
当结果不只是成功/失败,可以定义:
Kotlin
sealed interface UploadResult {
data object Ok : UploadResult
data class Fail(val code: Int?, val msg: String?) : UploadResult
}
typealias OnUpload = (UploadResult) -> Unit
这样更容易拓展成多状态结构。
✅ 3. 支持双回调形式(命令式写法)
Kotlin
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 可以让结构更清晰:
Kotlin
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:
-
✔ 可选回调
-
✔ 默认日志行为
-
✔ 可读可测
-
✔ 不传也安全
示例总结:
Kotlin
typealias OnResult = (Boolean) -> Unit
fun sendAndroidIdToServer(
uuid: String,
onSuc: OnResult = { success -> Log.d("MainVM", "result=$success") }
) { /* ... */ }
调用时:
Kotlin
sendAndroidIdToServer(deviceId) // 自动打印日志
sendAndroidIdToServer(deviceId) { ok -> ... } // 需要时写自定义回调
注意: 如果用下一种方式,默认回调被覆盖了,不会执行。
所以看不到 Log.d("MainVM", "result=$success") 这个日志。
最后一句
Kotlin 的高阶函数,不仅让回调更优雅,
也让「不用回调」变成了一种安全的设计习惯。