7、Flow在Repository层的应用:网络请求与数据库的Flow化、数据流合并、错误重试策略
好,我们直接进入正题。Repository 层,说白了就是数据的中转站。UI 层不关心数据是从网络来的,还是从本地数据库来的。它只关心一件事:给我数据,而且要稳。
我个人习惯,在 Repository 层全面拥抱 Flow。为什么?因为 Flow 天生就是为这种「数据随时间的连续变化」而生的。你想想看,网络请求的结果、数据库的更新,本质上都是事件流。用 Flow 包装它们,整个数据管道就变得非常优雅。
7.1 网络请求的 Flow 化
传统的网络请求,我们通常用 suspend 函数 + Retrofit。但如果你想让请求结果变成一个可观察的流,用 flow { } 构建器包一层就行。
我在项目中遇到过一个问题:用户下拉刷新时,如果网络请求失败,直接抛异常会导致 UI 层崩溃。所以,我习惯把网络请求包装成 Result 或者自定义的密封类。
// 网络请求 Flow 化的典型写法
fun fetchUserData(userId: String): Flow<Result<User>> = flow {
emit(Result.Loading) // 先发一个 Loading 状态
val response = apiService.getUser(userId)
if (response.isSuccessful) {
emit(Result.Success(response.body()!!))
} else {
emit(Result.Error(Exception("请求失败: ${response.code()}")))
}
}.catch { e ->
emit(Result.Error(e))
}
.catch 操作符捕获异常,而不是用 try-catch 包裹整个 flow。这样代码更简洁,而且不会丢失上游的取消信号。
7.2 数据库的 Flow 化
Room 从 2.1 版本开始就原生支持 Flow。你只需要在 DAO 方法中返回 Flow 类型,Room 会自动在表数据变化时重新发送查询结果。
嗯,这里要注意:Room 的 Flow 查询是「冷流」吗?其实是「热流」的变种。每次表数据变化,它都会重新执行查询并发射新数据。但它的生命周期和 collect 的生命周期绑定。
// Room DAO 的 Flow 化
@Dao
interface UserDao {
@Query("SELECT * FROM user WHERE id = :userId")
fun observeUser(userId: String): Flow<User?>
@Insert(onConflict = OnConflictStrategy.REPLACE)
suspend fun insertUser(user: User)
}
我曾经踩过一个坑:在 DAO 中返回 Flow<List<User>>,然后在 Repository 里用 .first() 取一次数据。结果发现每次数据库有变化,.first() 都会重新触发查询。后来我改用 StateFlow 做缓存,才解决了这个问题。
7.3 数据流合并:网络 + 数据库
这是 Repository 层的核心难点。我们通常的策略是:
- 先展示数据库中的缓存数据(如果有)
- 同时发起网络请求
- 网络请求成功后,更新数据库
- 数据库更新后,Flow 自动发射新数据
用 flatMapLatest 或者 combine 可以优雅地实现这个流程。我个人更推荐 flatMapLatest,因为它能处理「网络请求被新请求取消」的场景。
// Repository 层的数据流合并
fun getUserStream(userId: String): Flow<User> {
return userDao.observeUser(userId) // 数据库 Flow
.flatMapLatest { cachedUser ->
if (cachedUser != null) {
// 先发射缓存数据
flowOf(cachedUser)
} else {
emptyFlow()
}
}
.onStart {
// 发起网络请求
fetchUserFromNetwork(userId)
}
}
private suspend fun fetchUserFromNetwork(userId: String) {
val result = apiService.getUser(userId)
if (result.isSuccessful) {
userDao.insertUser(result.body()!!)
}
}
7.4 错误重试策略
网络请求不可能永远成功。重试策略是 Repository 层的必修课。Kotlin Flow 提供了 retry 和 retryWhen 操作符,用起来非常顺手。
我一般会这样设计重试逻辑:
- 网络错误(IOException):重试 3 次,每次间隔 2 秒
- 业务错误(服务器返回错误码):不重试,直接抛给 UI 层
- 超时错误:重试 1 次,间隔 5 秒
// 带重试策略的网络请求 Flow
fun fetchWithRetry(userId: String): Flow<Result<User>> = flow {
emit(Result.Loading)
val response = apiService.getUser(userId)
if (response.isSuccessful) {
emit(Result.Success(response.body()!!))
} else {
emit(Result.Error(Exception("业务错误: ${response.code()}")))
}
}.retryWhen { cause, attempt ->
// 只对网络异常进行重试
if (cause is IOException && attempt < 3) {
delay(2000) // 等待 2 秒
return@retryWhen true
}
return@retryWhen false // 其他错误不重试
}.catch { e ->
emit(Result.Error(e))
}
retryWhen 里忘记加 delay,结果重试瞬间发出 3 次请求,直接把服务器打挂了。记住:重试一定要有退避策略,至少加个 delay。
7.5 整体架构图
下面这张图展示了 Repository 层如何用 Flow 串联网络和数据库。我把它画成了 SVG,方便你理解数据流向。
7.6 实战中的避坑指南
最后,分享几个我在实际项目中踩过的坑:
- 不要在主线程做网络请求: Flow 默认在 collect 的线程执行。如果你在 ViewModel 中用
viewModelScope.launch收集,记得用.flowOn(Dispatchers.IO)切换线程。 - 小心背压: 如果数据库更新非常频繁(比如每秒几十次),Room 的 Flow 会发射大量数据。这时候可以用
.debounce(300)做防抖。 - StateFlow 和 SharedFlow 的选择: 如果 UI 层只需要最新值,用 StateFlow。如果需要多个订阅者且不关心初始值,用 SharedFlow。
MutableStateFlow 或 MutableSharedFlow。外部只能通过 collect 读取数据,不能直接修改。这是数据单向流动的基本原则。
好了,关于 Repository 层的 Flow 化,我就讲这么多。记住一个核心:数据流要单向,错误处理要分层,重试策略要谨慎。你想想看,只要把这三点做好,你的数据层基本上就稳了。