7、Flow在Repository层的应用:网络请求与数据库的Flow化、数据流合并、错误重试策略

好,我们直接进入正题。Repository 层,说白了就是数据的中转站。UI 层不关心数据是从网络来的,还是从本地数据库来的。它只关心一件事:给我数据,而且要稳。

我个人习惯,在 Repository 层全面拥抱 Flow。为什么?因为 Flow 天生就是为这种「数据随时间的连续变化」而生的。你想想看,网络请求的结果、数据库的更新,本质上都是事件流。用 Flow 包装它们,整个数据管道就变得非常优雅。

核心思想: Repository 层暴露的接口全部返回 Flow,调用方(ViewModel)只需要 collect,不需要关心数据来源和切换逻辑。

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 层的核心难点。我们通常的策略是:

  1. 先展示数据库中的缓存数据(如果有)
  2. 同时发起网络请求
  3. 网络请求成功后,更新数据库
  4. 数据库更新后,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()!!)
    }
}
注意: 上面的写法有一个潜在问题:如果网络请求失败,数据库没有更新,UI 层会一直显示旧数据。所以,我通常会在网络请求失败时,发射一个错误事件,让 UI 层决定是否显示 Snackbar。

7.4 错误重试策略

网络请求不可能永远成功。重试策略是 Repository 层的必修课。Kotlin Flow 提供了 retryretryWhen 操作符,用起来非常顺手。

我一般会这样设计重试逻辑:

  • 网络错误(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,方便你理解数据流向。

Repository 层 Flow 数据流架构 UI 层 (ViewModel) Repository 层 flatMapLatest + retryWhen 网络数据源 (Retrofit) 本地数据源 (Room) collect 网络请求 Flow 数据库 Flow 写入数据库 错误重试策略 网络异常 → retryWhen (最多3次, 间隔2秒) | 业务错误 → 直接抛给 UI 层 数据流方向:UI → Repository → 网络/数据库 → 合并 → UI

7.6 实战中的避坑指南

最后,分享几个我在实际项目中踩过的坑:

  • 不要在主线程做网络请求: Flow 默认在 collect 的线程执行。如果你在 ViewModel 中用 viewModelScope.launch 收集,记得用 .flowOn(Dispatchers.IO) 切换线程。
  • 小心背压: 如果数据库更新非常频繁(比如每秒几十次),Room 的 Flow 会发射大量数据。这时候可以用 .debounce(300) 做防抖。
  • StateFlow 和 SharedFlow 的选择: 如果 UI 层只需要最新值,用 StateFlow。如果需要多个订阅者且不关心初始值,用 SharedFlow。
重要提醒: 不要在 Repository 层暴露 MutableStateFlowMutableSharedFlow。外部只能通过 collect 读取数据,不能直接修改。这是数据单向流动的基本原则。

好了,关于 Repository 层的 Flow 化,我就讲这么多。记住一个核心:数据流要单向,错误处理要分层,重试策略要谨慎。你想想看,只要把这三点做好,你的数据层基本上就稳了。

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