16、协程与Retrofit网络请求:Retrofit的suspend函数支持、网络请求的协程封装、错误处理与重试机制、请求取消与生命周期绑定

网络请求,说白了就是App跟服务器打交道。在Android里,Retrofit几乎是标配。但以前用回调的方式写网络请求,那叫一个痛苦——回调地狱、线程切换、生命周期管理……每个坑我都踩过。

协程出现后,这一切都变了。Retrofit从2.6.0版本开始原生支持suspend函数,这意味着我们可以用同步的方式写异步代码。嗯,今天我就带你彻底搞懂协程+Retrofit的最佳实践。

Retrofit的suspend函数支持

先看最基础的东西。Retrofit怎么定义suspend接口?

interface ApiService {
    @GET("user/{id}")
    suspend fun getUser(@Path("id") id: String): User

    @POST("user/login")
    suspend fun login(@Body request: LoginRequest): LoginResponse
}

就这么简单。返回类型直接写成你想要的实体类,不需要Call<User>,不需要enqueue。调用的时候,直接在一个协程里调用就行。

我记得第一次用这个特性时,心里还有点打鼓——这真的能行?后来看了源码才发现,Retrofit内部会自动帮我们做线程切换。网络请求在IO线程执行,结果回到调用协程的上下文。说白了,就是帮我们省掉了withContext(Dispatchers.IO)这一步。

小提示:如果你的Retrofit版本低于2.6.0,需要升级。我建议直接用最新稳定版,老版本不支持suspend。

网络请求的协程封装

直接调用ApiService当然可以,但不够优雅。我个人习惯做一层封装,把网络请求的通用逻辑抽出来。

class NetworkRepository(private val api: ApiService) {

    suspend fun <T> safeApiCall(call: suspend () -> T): Result<T> {
        return try {
            Result.success(call.invoke())
        } catch (e: Exception) {
            Result.failure(e)
        }
    }

    suspend fun getUser(id: String): Result<User> {
        return safeApiCall { api.getUser(id) }
    }
}

这里用Result包装返回值,好处是调用方不用再写try-catch。你想想看,如果每个网络请求都要写一遍异常处理,那代码得多冗余。

我在项目中还遇到过一种情况:多个接口需要组合调用。比如先登录,拿到token后再请求用户信息。用协程写起来就特别自然:

suspend fun loginAndGetUser(loginRequest: LoginRequest): Result<User> {
    return safeApiCall {
        val loginResponse = api.login(loginRequest)
        api.getUser(loginResponse.token)
    }
}

这就是协程的魅力——顺序写代码,背后却是异步执行。

错误处理与重试机制

网络请求不可能永远成功。服务器挂了、网络断了、超时了……这些都要处理。我见过太多App因为没做好错误处理,直接闪退或者白屏。

先说说错误处理的层次。我一般分三层:

层次 处理内容 示例
网络层 连接超时、DNS解析失败 SocketTimeoutException
协议层 HTTP状态码异常 HttpException(404)
业务层 服务器返回的业务错误码 {"code":1001,"msg":"token过期"}

针对网络层和协议层的错误,我建议做重试。但重试不是无脑重试,要有策略。

suspend fun <T> retryWithBackoff(
    maxRetries: Int = 3,
    initialDelay: Long = 1000,
    factor: Double = 2.0,
    block: suspend () -> T
): T {
    var currentDelay = initialDelay
    repeat(maxRetries) { attempt ->
        try {
            return block()
        } catch (e: Exception) {
            if (attempt == maxRetries - 1) throw e
            delay(currentDelay)
            currentDelay = (currentDelay * factor).toLong()
        }
    }
    throw IllegalStateException("不应该执行到这里")
}

这个重试策略用了指数退避。第一次失败等1秒,第二次等2秒,第三次等4秒。为什么要这样?因为服务器可能正在恢复中,你疯狂重试反而加重负担。

注意:不是所有错误都适合重试。比如401未授权,重试多少次都没用。我建议只对网络层错误和5xx服务器错误做重试。

我曾经犯过一个错误:对所有异常都重试。结果用户token过期了,App还在那疯狂重试登录接口,搞得服务器日志全是401。后来我加了个判断:

if (e is HttpException && e.code() in 400..499) {
    throw e // 客户端错误,不重试
}

请求取消与生命周期绑定

这个点特别重要。用户退出页面了,网络请求还在跑,不仅浪费资源,还可能导致内存泄漏甚至崩溃。

协程的取消机制天然支持这一点。关键是要把协程的生命周期和组件的生命周期绑定。

在ViewModel里,我们用viewModelScope

class UserViewModel : ViewModel() {
    private val repo = NetworkRepository(ApiClient.create())

    fun loadUser(id: String) {
        viewModelScope.launch {
            val result = repo.getUser(id)
            // 更新UI
        }
    }
}

当ViewModel被清除时,viewModelScope会自动取消所有子协程。不需要手动管理。

在Activity或Fragment里,可以用lifecycleScope

class UserFragment : Fragment() {
    override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
        super.onViewCreated(view, savedInstanceState)
        
        lifecycleScope.launch {
            // 网络请求
        }
    }
}

但这里有个坑:lifecycleScope默认在Lifecycle.State.STARTED之后才会启动协程,在Lifecycle.State.DESTROYED时取消。如果你在onCreate里启动协程,它可能不会立即执行。

我个人习惯用repeatOnLifecycle

lifecycleScope.launch {
    repeatOnLifecycle(Lifecycle.State.STARTED) {
        // 这个协程会在STARTED时启动,STOPPED时取消
        // 非常适合做UI相关的网络请求
    }
}

你想想看,用户切到后台,协程自动取消;用户切回来,重新启动。完美解决了生命周期问题。

核心要点:协程的取消是协作式的。如果你的网络请求库不支持取消(比如某些老版本的OkHttp),即使协程被取消了,网络请求可能还在执行。Retrofit底层用的OkHttp,从3.x版本开始就支持取消,所以放心用。

最后,我画了一张图,把整个知识体系串起来:

协程 + Retrofit 网络请求知识体系 协程 + Retrofit suspend函数支持 接口定义:suspend fun 自动线程切换 协程封装 safeApiCall + Result 组合调用 错误处理与重试 三层错误分类 指数退避重试 请求取消与生命周期 viewModelScope lifecycleScope repeatOnLifecycle 核心:结构化并发 + 生命周期感知

这张图把四个核心知识点串在了一起。从suspend函数支持开始,到协程封装,再到错误处理和重试,最后落到生命周期绑定。你写代码的时候,脑子里要有这张图。

好了,这一章的内容就到这里。记住:网络请求不是简单的CRUD,错误处理和生命周期管理才是体现功力的地方。下次写网络请求时,多想想——如果用户切到后台怎么办?如果服务器挂了怎么办?把这些想清楚,你的代码质量会上一个台阶。


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