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 官方推荐的是 ResponseResult 类型。

先看一个最简单的例子:

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 是冷流:每次订阅都会触发网络请求。如果你不想重复请求,可以用 shareInstateIn 把它变成热流。
  • 取消订阅:Flow 会在协程取消时自动取消网络请求。但前提是你用的是协程的 withContextlaunch。如果你在 Flow 里用了阻塞代码,那就没法自动取消了。
  • 错误恢复:Flow 提供了 retry 操作符,可以很方便地实现网络重试。我一般会设置最多重试 3 次,每次间隔 2 秒。
api.getUsersFlow()
    .retry(3) { cause ->
        // 只有网络异常才重试,业务错误不重试
        cause is IOException
    }
    .asNetworkResult()

你想想看,如果用户在地铁里信号不好,自动重试几次,体验是不是好很多?

知识体系图

下面这张图,展示了 Flow 与 Retrofit 结合的核心逻辑:

Flow + Retrofit 网络请求架构 Retrofit API 接口 Flow 转换 (asNetworkResult) NetworkResult 状态分发 (Loading/Success/Error) ViewModel 订阅更新 UI 关键点 • 冷流:每次订阅触发请求 • 自动取消:协程取消时 • 统一错误:Repository 层 • 重试机制:retry 操作符 • 状态封装:密封类 • 热流转换:shareIn

注意: 不要直接在 UI 层订阅网络请求的 Flow。一定要经过 ViewModel 或 Repository 的转换,把 Loading、Success、Error 状态都处理好。否则,一旦网络错误,你的 App 可能直接崩溃。

好了,这一章的内容就到这里。Flow 和 Retrofit 的组合,说白了就是让网络请求变得更「响应式」。你不需要手动管理回调,不需要担心内存泄漏,只需要关注数据流的变化。我个人觉得,这是目前 Android 网络层最优雅的写法之一。

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