Kotlin Flow离线状态恢复实战 - 第3节

3.1 模式原理与核心职责

NetworkBoundResource 是一种桥接模式,用于在单数据源架构中统一管理网络与本地数据流。它的核心职责可拆解为三个维度:

  • 资源边界管理:定义网络请求与本地缓存的交互边界,防止数据流混乱。
  • 状态封装:将异步数据流包装为统一的密封类,包含 Loading、Success、Error、Offline 四种状态。
  • 离线优先:优先返回本地缓存数据,网络请求仅在需要时触发,并在失败时自动降级为离线状态。
状态含义典型场景
Loading数据正在加载中首次加载、网络请求进行中
Success数据加载成功本地缓存存在或网络请求成功
Error请求发生错误网络异常、解析失败、服务器错误
Offline无网络且本地无缓存设备离线且本地数据为空

3.2 实现流程:网络-本地数据流桥接

标准实现流程分为四个阶段,每个阶段对应一个数据源操作:

  1. 查询本地缓存:从 Room 或 DataStore 发射初始数据。
  2. 判断是否应从网络获取:根据缓存时效、用户手势或网络可用性决定是否发起网络请求。
  3. 发起网络请求并映射结果:调用 Retrofit API,将响应转换为领域模型。
  4. 保存到本地并重启数据流:将网络结果写入数据库,触发 Flow 重新发射最新数据。

整个流程由 channelFlow 或 flatMapLatest 桥接,保持响应式链。

⚠️ 注意:桥接过程中必须处理背压和取消信号。建议使用 callbackFlow 包装原生回调 API,或使用 channelFlow 手动管理协程作用域。
NetworkBoundResource 状态流桥接图 查询本地缓存 判断网络需求 本地缓存成功 发起网络请求 处理网络结果 保存到本地 发射最终状态 Offline 状态 网络失败自动降级为 Offline

3.3 核心代码实现

以下展示 Kotlin 中典型的 NetworkBoundResource 骨架,使用 Flow 封装状态:

sealed class Resource<out T> {
    data class Loading<T>(val data: T? = null) : Resource<T>()
    data class Success<T>(val data: T) : Resource<T>()
    data class Error<T>(val message: String, val data: T? = null) : Resource<T>()
    data class Offline<T>(val data: T? = null) : Resource<T>()
}

abstract class NetworkBoundResource<ResultType, RequestType> {
    fun asFlow(): Flow<Resource<ResultType>> = channelFlow {
        // 1. 发射本地缓存
        val localData = loadFromLocal()
        if (localData != null) {
            send(Resource.Loading(localData))
        } else {
            send(Resource.Loading())
        }

        // 2. 判断是否需要网络获取
        if (!shouldFetch(localData)) {
            send(Resource.Success(localData))
            close()
            return@channelFlow
        }

        // 3. 发起网络请求
        try {
            val response = createCall()
            val body = processResponse(response)
            // 4. 保存到本地
            saveToLocal(body)
            // 5. 重新发射本地数据
            val updatedData = loadFromLocal()
            send(Resource.Success(updatedData))
        } catch (e: IOException) {
            // 网络异常 → 离线状态
            if (localData != null) {
                send(Resource.Error(e.message ?: "网络错误", localData))
            } else {
                send(Resource.Offline())
            }
        }
    }

    protected abstract fun loadFromLocal(): ResultType?
    protected abstract fun shouldFetch(data: ResultType?): Boolean
    protected abstract suspend fun createCall(): RequestType
    protected abstract fun processResponse(response: RequestType): ResultType
    protected abstract suspend fun saveToLocal(data: ResultType): Unit
}

3.4 调试与验证

开发阶段可使用以下方法验证状态流正确性:

  • 日志插桩:在 send() 前后打印当前状态和线程。
  • 单元测试:通过 TestCoroutineScope 模拟网络延时和异常。
  • 网络模拟:使用 Charles 或 MockWebServer 模拟离线场景。
💡 调试技巧:在 channelFlow 内添加 try/catch 捕获取消异常 CancellationException,避免协程泄漏。

3.5 常见错误与优化

问题原因解决方案
状态重复发射 Loading未检查本地缓存是否为空先判断本地数据,再发射 Loading 或 Success
网络失败后仍显示旧数据未包裹本地数据在 Error 状态中Error 状态携带本地 cache 数据
Offline 状态缺失未区分“无网络 + 无缓存”在 IOException 且本地数据为 null 时发射 Offline
背压导致数据丢失collect 消费速度慢于发送使用 conflate 或 buffer

稳定性优化建议:

  • 为本地缓存添加 TTL 过期机制,避免频繁网络请求。
  • 在网络请求层使用 retry 操作符(如 retryWhen)处理瞬时错误。
  • 对于 UI 层收集,使用 stateIn(scope, SharingStarted.WhileSubscribed()) 避免重复激活。