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协程方式
@GET("users/{id}")
fun getUser(@Path("id") id: Int): Call<User>
@GET("users/{id}")
suspend fun getUser(@Path("id") id: Int): User

看到区别了吗?协程版本直接返回 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 ?: "未知错误")
            }
        }
    }
}

你看,用 asyncawait 就能轻松实现并发请求,而且代码是顺序书写的,可读性非常好。如果换成回调方式,这段代码至少要多写两倍的样板代码。

注意:使用 async 时,要确保在同一个 CoroutineScope 中。如果每个 async 都使用不同的scope,可能会导致请求无法被统一取消。

SVG:协程网络请求核心流程

协程网络请求核心流程 ViewModel viewModelScope.launch async { api.getUser() } async { api.getPosts() } async { api.getFollowers() } 组合数据,更新UI

常见问题与避坑指南

  • 协程取消后网络请求还在继续? 我曾经遇到过这个问题。原因是OkHttp的Call没有被取消。解决方案是在 suspendCancellableCoroutine 中注册 invokeOnCancellation 回调,手动取消Call。
  • 多个接口并发时,一个失败导致全部失败? 默认情况下,async 的异常会传播到父协程。如果你希望某个接口失败不影响其他接口,可以使用 supervisorScope 包裹。
  • Retrofit的suspend函数不支持超时? 其实可以通过OkHttp的 connectTimeoutreadTimeout 等配置来实现。或者用 withTimeout 协程函数包裹请求。

个人建议:在团队项目中,统一封装一个网络请求的基类或扩展库,把错误处理、重试机制、日志打印等通用逻辑收敛起来。这样每个业务开发只需要关注数据转换和UI更新,效率会高很多。

好了,关于Network KTX的内容就聊到这里。协程 + Retrofit的组合,确实让网络请求的代码变得清爽了不少。你可以在自己的项目里试试看,把那些老旧的回调式网络请求逐步迁移过来,体验一下「丝滑」的感觉。

公众号:蓝海资料掘金营,微信deep3321