27、Network KTX:OkHttp与Retrofit的KTX扩展,协程网络请求
网络请求,几乎是每个Android应用都绕不开的坎。我记得刚入行那会儿,用AsyncTask做网络请求,回调嵌套得跟千层饼似的。后来Retrofit + RxJava成了标配,虽然比之前好多了,但学习曲线还是有点陡。
直到Kotlin协程和KTX扩展出现,网络请求才真正变得「优雅」起来。今天我们就聊聊OkHttp和Retrofit的KTX扩展,看看怎么用协程把网络请求写得既简洁又健壮。
为什么需要Network KTX?
说白了,原生Retrofit和OkHttp的API设计,并没有为协程做优化。你想想看,每次调用接口都要手动处理线程切换、异常捕获、资源释放……这些重复劳动,其实完全可以交给扩展函数去搞定。
我在项目中遇到过这样一个场景:一个列表页需要同时请求三个接口,等全部返回后再刷新UI。如果用回调的方式,代码会变得非常丑陋。而KTX扩展配合协程,几行代码就解决了。
核心价值:Network KTX让网络请求与协程无缝集成,减少样板代码,提升可读性和健壮性。
OkHttp的KTX扩展
OkHttp本身并没有官方的KTX库,但我们可以通过一些扩展函数来优化它的使用体验。我个人习惯封装一个简单的扩展,让OkHttp的异步请求直接返回协程的Deferred或Flow。
基础扩展:将Call转为协程
// 扩展函数:将OkHttp的Call转为协程的Deferred
suspend fun Call.await(): Response {
return suspendCancellableCoroutine { continuation ->
enqueue(object : Callback {
override fun onResponse(call: Call, response: Response) {
continuation.resume(response)
}
override fun onFailure(call: Call, e: IOException) {
continuation.resumeWithException(e)
}
})
// 协程取消时,自动取消网络请求
continuation.invokeOnCancellation {
if (!isCanceled()) cancel()
}
}
}
嗯,这里要注意一点:suspendCancellableCoroutine 是协程提供的挂起函数,它允许我们在回调中恢复协程的执行。而且它自带取消监听,当协程被取消时,会自动取消网络请求,避免资源浪费。
小技巧:如果你需要流式响应(比如下载大文件),可以用 callFlow() 扩展,把每次数据块发射到Flow中,配合协程的collect消费。
Retrofit的KTX扩展
Retrofit从2.6.0版本开始,官方就支持了协程。你只需要在接口定义中返回 suspend 函数,或者返回 Response<T> 或 Call<T> 的包装类型。
接口定义对比
| 传统方式 | KTX协程方式 |
|---|---|
|
|
看到区别了吗?协程版本直接返回 User 对象,而不是 Call<User>。这意味着你不需要手动调用 enqueue() 或 execute(),Retrofit内部会自动处理异步请求。
错误处理的最佳实践
我曾经踩过一个坑:直接用 suspend 函数返回数据,如果接口返回非2xx状态码,会直接抛出 HttpException。这在某些场景下不够灵活。
我建议的做法是:
// 接口定义:返回Response包装类型
@GET("users/{id}")
suspend fun getUser(@Path("id") id: Int): Response<User>
// 调用时手动处理
viewModelScope.launch {
val response = api.getUser(123)
if (response.isSuccessful) {
val user = response.body()
// 更新UI
} else {
// 处理错误,比如显示Toast
Log.e("Network", "Error: ${response.code()}")
}
}
这样做的好处是,你可以精确控制错误处理逻辑,而不是依赖全局的异常捕获。当然,如果你觉得每次都写 isSuccessful 判断太啰嗦,也可以封装一个扩展函数:
suspend fun <T> Response<T>.awaitBody(): T {
if (!isSuccessful) {
throw HttpException(this)
}
return body() ?: throw NullPointerException("Response body is null")
}
协程网络请求的完整示例
我们来看一个实际项目中的例子。假设我们要实现一个用户列表页,需要从三个接口获取数据:用户信息、用户帖子、用户粉丝数。等三个接口都返回后,再刷新UI。
// ViewModel中
class UserViewModel : ViewModel() {
private val api = RetrofitClient.instance
fun loadUserData(userId: Int) {
viewModelScope.launch {
try {
// 并发请求三个接口
val userDeferred = async { api.getUser(userId) }
val postsDeferred = async { api.getPosts(userId) }
val followersDeferred = async { api.getFollowers(userId) }
// 等待所有请求完成
val user = userDeferred.await()
val posts = postsDeferred.await()
val followers = followersDeferred.await()
// 组合数据,更新UI
_uiState.value = UiState.Success(
user = user,
posts = posts,
followers = followers
)
} catch (e: Exception) {
_uiState.value = UiState.Error(e.message ?: "未知错误")
}
}
}
}
你看,用 async 和 await 就能轻松实现并发请求,而且代码是顺序书写的,可读性非常好。如果换成回调方式,这段代码至少要多写两倍的样板代码。
注意:使用 async 时,要确保在同一个 CoroutineScope 中。如果每个 async 都使用不同的scope,可能会导致请求无法被统一取消。
SVG:协程网络请求核心流程
常见问题与避坑指南
- 协程取消后网络请求还在继续? 我曾经遇到过这个问题。原因是OkHttp的Call没有被取消。解决方案是在
suspendCancellableCoroutine中注册invokeOnCancellation回调,手动取消Call。 - 多个接口并发时,一个失败导致全部失败? 默认情况下,
async的异常会传播到父协程。如果你希望某个接口失败不影响其他接口,可以使用supervisorScope包裹。 - Retrofit的suspend函数不支持超时? 其实可以通过OkHttp的
connectTimeout、readTimeout等配置来实现。或者用withTimeout协程函数包裹请求。
个人建议:在团队项目中,统一封装一个网络请求的基类或扩展库,把错误处理、重试机制、日志打印等通用逻辑收敛起来。这样每个业务开发只需要关注数据转换和UI更新,效率会高很多。
好了,关于Network KTX的内容就聊到这里。协程 + Retrofit的组合,确实让网络请求的代码变得清爽了不少。你可以在自己的项目里试试看,把那些老旧的回调式网络请求逐步迁移过来,体验一下「丝滑」的感觉。