12、Flow与Retrofit:Retrofit的Flow支持、网络请求的Flow化、统一错误处理
说实话,Retrofit 和 Flow 的组合,是我这两年做 Android 项目最常用的网络请求方案。以前用 Callback 或者 RxJava,总感觉代码里塞满了模板代码。后来 Retrofit 官方开始支持 Kotlin 协程,再配合 Flow,整个网络层写起来就舒服多了。
这一章,我们就来聊聊怎么把 Retrofit 和 Flow 结合起来。我会从最基础的用法讲起,然后深入到统一错误处理。嗯,这部分坑不少,我踩过好几次。
Retrofit 的 Flow 支持
Retrofit 从 2.6.0 版本开始,就支持了 Kotlin 协程的 suspend 函数。但 Flow 的支持,其实是通过自定义 CallAdapter 来实现的。不过别担心,官方已经帮我们做好了。
你只需要在 Retrofit 的构建器里,添加一个 Flow 的 CallAdapter 工厂。我个人习惯用 kotlinx.coroutines.flow 包里的 flow 构建器,但 Retrofit 官方推荐的是 Response 和 Result 类型。
先看一个最简单的例子:
interface ApiService {
@GET("users")
suspend fun getUsers(): List<User>
@GET("users")
fun getUsersFlow(): Flow<List<User>>
}
注意看,第二个方法返回的是 Flow<List<User>>。这就是 Retrofit 对 Flow 的原生支持。你不需要额外配置,只要 Retrofit 的版本够新就行。
核心要点: Retrofit 的 Flow 支持,本质上是将一次网络请求包装成一个冷流。每次有观察者订阅,就会触发一次网络请求。
网络请求的 Flow 化
在实际项目中,我们很少直接返回原始数据。更多时候,我们需要处理 loading 状态、错误信息、缓存策略等。这时候,Flow 的优势就体现出来了。
我一般会定义一个 NetworkResult 密封类,用来封装网络请求的三种状态:
sealed class NetworkResult<out T> {
data class Success<T>(val data: T) : NetworkResult<T>()
data class Error(val message: String, val code: Int? = null) : NetworkResult<Nothing>()
object Loading : NetworkResult<Nothing>()
}
然后,我会写一个扩展函数,把 Retrofit 的 Flow 转换成带状态的 Flow:
fun <T> Flow<T>.asNetworkResult(): Flow<NetworkResult<T>> = flow {
emit(NetworkResult.Loading)
try {
val data = collect()
emit(NetworkResult.Success(data))
} catch (e: Exception) {
emit(NetworkResult.Error(e.message ?: "未知错误"))
}
}
等等,上面这个写法其实有问题。因为 collect 是终端操作,不能直接这样用。正确的做法是使用 catch 操作符和 map 操作符:
fun <T> Flow<T>.asNetworkResult(): Flow<NetworkResult<T>> = this
.map<T, NetworkResult<T>> { NetworkResult.Success(it) }
.onStart { emit(NetworkResult.Loading) }
.catch { emit(NetworkResult.Error(it.message ?: "网络错误")) }
这样写就对了。每次网络请求,都会先发射一个 Loading 状态,然后要么是 Success,要么是 Error。
小技巧: 如果你需要处理 HTTP 状态码,可以在 catch 块里判断异常类型。比如 HttpException 可以拿到具体的状态码。
统一错误处理
统一错误处理,说白了就是让所有网络请求的错误,都走同一个处理逻辑。这样你就不用在每个 ViewModel 里重复写 try-catch 了。
我常用的做法是,在 Repository 层做一次统一的错误转换。举个例子:
class UserRepository(private val api: ApiService) {
fun getUsers(): Flow<NetworkResult<List<User>>> {
return api.getUsersFlow()
.asNetworkResult()
.map { result ->
when (result) {
is NetworkResult.Error -> {
// 统一处理错误,比如记录日志、显示 Toast
val friendlyMessage = when (result.code) {
401 -> "登录已过期,请重新登录"
403 -> "没有权限访问"
404 -> "资源不存在"
500 -> "服务器繁忙,请稍后重试"
else -> result.message
}
NetworkResult.Error(friendlyMessage, result.code)
}
else -> result
}
}
}
}
这样做的好处是,ViewModel 层只需要订阅 Flow,然后根据不同的状态更新 UI 即可。错误处理逻辑都集中在 Repository 里,维护起来很方便。
我曾经在一个项目里,看到每个 ViewModel 都自己写 try-catch,结果错误提示五花八门。有的显示「网络异常」,有的显示「请求失败」,用户看了都懵。后来我统一改成了上面的模式,体验好了很多。
实战中的注意事项
嗯,这里有几个坑,我提一下:
- Flow 是冷流:每次订阅都会触发网络请求。如果你不想重复请求,可以用
shareIn或stateIn把它变成热流。 - 取消订阅:Flow 会在协程取消时自动取消网络请求。但前提是你用的是协程的
withContext或launch。如果你在 Flow 里用了阻塞代码,那就没法自动取消了。 - 错误恢复:Flow 提供了
retry操作符,可以很方便地实现网络重试。我一般会设置最多重试 3 次,每次间隔 2 秒。
api.getUsersFlow()
.retry(3) { cause ->
// 只有网络异常才重试,业务错误不重试
cause is IOException
}
.asNetworkResult()
你想想看,如果用户在地铁里信号不好,自动重试几次,体验是不是好很多?
知识体系图
下面这张图,展示了 Flow 与 Retrofit 结合的核心逻辑:
注意: 不要直接在 UI 层订阅网络请求的 Flow。一定要经过 ViewModel 或 Repository 的转换,把 Loading、Success、Error 状态都处理好。否则,一旦网络错误,你的 App 可能直接崩溃。
好了,这一章的内容就到这里。Flow 和 Retrofit 的组合,说白了就是让网络请求变得更「响应式」。你不需要手动管理回调,不需要担心内存泄漏,只需要关注数据流的变化。我个人觉得,这是目前 Android 网络层最优雅的写法之一。