为什么需要封装协程?
写过Android或者后端Kotlin项目的人应该都有体会,刚开始用协程时觉得特别香,不用再回调地狱了。可时间一长,问题就来了:每个网络请求都要写一遍launch、try-catch、主线切换,重复代码堆得跟面条一样。
比如你做个登录功能,要调接口、转主线更新UI、处理异常弹Toast,三个地方都这么写一遍,到第六个页面你自己都想删代码了。
一个常见的“裸奔”写法
viewModelScope.launch {
try {
val result = repository.login(username, password)
withContext(Dispatchers.Main) {
// 更新UI
showSuccess(result)
}
} catch (e: Exception) {
withContext(Dispatchers.Main) {
showToast(e.message)
}
}
}这种写法在项目里复制粘贴几次,维护起来就很头疼。一旦需求改了,比如要统一加埋点或日志,就得一个个翻文件去改。
封装成一个扩展函数
我们可以把通用逻辑抽出来,变成一个扩展函数。比如叫 launchWithCatch:
inline fun CoroutineScope.launchWithCatch(
crossinline onError: (String) -> Unit = {},
noinline block: suspend CoroutineScope.() -> Unit
) = launch {
try {
block()
} catch (e: Exception) {
withContext(Dispatchers.Main) {
onError(e.message ?: "未知错误")
}
}
}然后调用的地方就干净多了:
launchWithCatch { toast ->
showToast(toast)
} {
val result = repository.login(username, password)
withContext(Dispatchers.Main) {
showSuccess(result)
}
}这时候你会发现,业务代码一眼就能看懂,出错处理也集中管理了。如果哪天产品说“所有错误都打个日志”,你只需要改这一个函数就行。
配合LiveData或StateFlow更顺手
很多项目用LiveData展示数据状态,比如加载中、成功、失败。可以再封装一个自动切换主线并发送状态的版本:
fun <T> LiveData<Result<T>>.launchFlow(
block: suspend () -> T
) = liveDataScope.launch {
emit(Result.Loading)
try {
val data = block()
emit(Result.Success(data))
} catch (e: Exception) {
emit(Result.Error(e.message))
}
}调用时直接绑定到UI状态:
loginResult.launchFlow {
repository.login(username, password)
}XML里用observe监听 loginResult,自动处理Loading和Error状态,Activity/Fragment几乎不用写逻辑。
实际场景:电商App的商品详情页
打开商品页要干一堆事:拉商品信息、查库存、推荐列表、用户评价。以前得嵌套回调或者写一堆job变量控制完成状态,现在用封装后的协程,可以这么写:
launchWithCatch { msg ->
showError(msg)
} {
val product = async { repo.fetchProduct(id) }
val stock = async { repo.checkStock(id) }
val reviews = async { repo.getReviews(id) }
// 等全部返回
updateUI(product.await(), stock.await(), reviews.await())
}既保证了并发效率,又没丢失可读性。用户看到的是秒开的页面,而你写的代码也不再是“一次性胶布代码”。
好的封装不是为了炫技,而是让团队里新手也能写出结构一致、容错性强的代码。Kotlin协程本身已经很强大,再加一层合适的包装,日常开发才能真正省心。