Kotlin Flow离线状态恢复实战 - 第3节
3.1 模式原理与核心职责
NetworkBoundResource 是一种桥接模式,用于在单数据源架构中统一管理网络与本地数据流。它的核心职责可拆解为三个维度:
- 资源边界管理:定义网络请求与本地缓存的交互边界,防止数据流混乱。
- 状态封装:将异步数据流包装为统一的密封类,包含 Loading、Success、Error、Offline 四种状态。
- 离线优先:优先返回本地缓存数据,网络请求仅在需要时触发,并在失败时自动降级为离线状态。
| 状态 | 含义 | 典型场景 |
|---|---|---|
| Loading | 数据正在加载中 | 首次加载、网络请求进行中 |
| Success | 数据加载成功 | 本地缓存存在或网络请求成功 |
| Error | 请求发生错误 | 网络异常、解析失败、服务器错误 |
| Offline | 无网络且本地无缓存 | 设备离线且本地数据为空 |
3.2 实现流程:网络-本地数据流桥接
标准实现流程分为四个阶段,每个阶段对应一个数据源操作:
- 查询本地缓存:从 Room 或 DataStore 发射初始数据。
- 判断是否应从网络获取:根据缓存时效、用户手势或网络可用性决定是否发起网络请求。
- 发起网络请求并映射结果:调用 Retrofit API,将响应转换为领域模型。
- 保存到本地并重启数据流:将网络结果写入数据库,触发 Flow 重新发射最新数据。
整个流程由 channelFlow 或 flatMapLatest 桥接,保持响应式链。
⚠️ 注意:桥接过程中必须处理背压和取消信号。建议使用
callbackFlow 包装原生回调 API,或使用 channelFlow 手动管理协程作用域。
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())避免重复激活。