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)这一步。
网络请求的协程封装
直接调用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秒。为什么要这样?因为服务器可能正在恢复中,你疯狂重试反而加重负担。
我曾经犯过一个错误:对所有异常都重试。结果用户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相关的网络请求
}
}
你想想看,用户切到后台,协程自动取消;用户切回来,重新启动。完美解决了生命周期问题。
最后,我画了一张图,把整个知识体系串起来:
这张图把四个核心知识点串在了一起。从suspend函数支持开始,到协程封装,再到错误处理和重试,最后落到生命周期绑定。你写代码的时候,脑子里要有这张图。
好了,这一章的内容就到这里。记住:网络请求不是简单的CRUD,错误处理和生命周期管理才是体现功力的地方。下次写网络请求时,多想想——如果用户切到后台怎么办?如果服务器挂了怎么办?把这些想清楚,你的代码质量会上一个台阶。