4. 网络数据源实现:Retrofit接口定义、Response封装、suspend函数与Flow转换
网络数据源是缓存架构中“远端数据”的唯一出口。本章围绕 Retrofit 如何定义接口、如何封装通用 Response、如何通过 suspend 函数获取一次快照以及如何将 suspend 转为 Flow 实现全生命周期观测,拆解完整链路。
4.1 整体流程
客户端请求经过三层转换:API 定义 → 悬浮挂起 → 流化推送。下图展示从 Retrofit 接口到 Flow 输出的核心节点与数据流向。
4.2 Retrofit 接口定义
使用 Retrofit 2.6+ 原生支持 suspend 函数。优先在接口方法上添加 @Headers、@Query 或 @Path,并保持返回值类型为 Response<T>,方便手动处理 HTTP 状态码。
// ApiService.kt
interface ApiService {
@GET("user/profile")
suspend fun getProfile(
@Query("userId") userId: String
): Response<UserProfileResponse>
@POST("user/update")
suspend fun updateProfile(
@Body request: UpdateRequest
): Response<UserProfileResponse>
}
📌 最佳实践: 始终返回
Response<T> 而不是直接返回 T,这样可以在 Repository 层获取 code、headers 以及 errorBody(),避免 Retrofit 自动抛出异常导致 Flow 中断。
4.3 Response 封装
定义一个通用数据类统一承载成功/失败信息。示例包含状态码、错误消息和数据体。
// NetworkResponse.kt
sealed class NetworkResponse<out T> {
data class Success<T>(val data: T) : NetworkResponse<T>()
data class Error(val code: Int, val message: String?) : NetworkResponse<Nothing>()
data class Exception(val throwable: Throwable) : NetworkResponse<Nothing>()
}
借助 Kotlin 扩展函数将 Retrofit 的 Response 转换为自定义封装:
suspend fun <T> safeApiCall(apiCall: suspend () -> Response<T>): NetworkResponse<T> {
return try {
val response = apiCall()
if (response.isSuccessful) {
val body = response.body()
if (body != null) NetworkResponse.Success(body)
else NetworkResponse.Error(response.code(), "Body is null")
} else {
NetworkResponse.Error(response.code(), response.errorBody()?.string())
}
} catch (e: Exception) {
NetworkResponse.Exception(e)
}
}
4.4 suspend 函数与 Flow 转换
Repository 层通过 flow { } 构造器将单次 suspend 调用转为冷流。每次收集时都会执行一次网络请求,适合“网络请求 + 缓存更新”场景。
// UserRepository.kt
class UserRepository(private val api: ApiService) {
fun fetchProfile(userId: String): Flow<NetworkResponse<UserProfileResponse>> = flow {
val result = safeApiCall { api.getProfile(userId) }
emit(result)
}
// 带重试和超时控制的增强版本
fun fetchProfileWithRetry(userId: String): Flow<NetworkResponse<UserProfileResponse>> = flow {
var attempts = 0
var lastResult: NetworkResponse<UserProfileResponse>? = null
while (attempts < 3) {
val result = safeApiCall { api.getProfile(userId) }
if (result is NetworkResponse.Success) {
emit(result)
return@flow
}
lastResult = result
attempts++
delay(1000L * attempts) // 指数退避
}
emit(lastResult ?: NetworkResponse.Exception(IOException("Max retries reached")))
}.retry(2) { e ->
e is IOException && e.message?.contains("timeout") == true
}
}
4.5 Flow 转换中的异常处理
直接在 flow { } 内使用 try-catch,避免异常泄漏导致 Flow 结束。亦可使用 catch 操作符:
fun fetchProfileSafe(userId: String): Flow<NetworkResponse<UserProfileResponse>> =
flow {
val result = safeApiCall { api.getProfile(userId) }
emit(result)
}.catch { e ->
emit(NetworkResponse.Exception(e))
}
⚠️ 性能注意: 不要在
flow { } 中使用 blocking 调用或同步网络请求。确保 safeApiCall 内只使用 suspend 函数,否则会阻塞协程调度器,引发线程饥饿。
4.6 常见错误与调试
| 错误场景 | 原因 | 调试方法 |
|---|---|---|
| Flow 不发射数据 | flow 构造器未调用 emit() 或提前 return | 在 emit 前后加日志,检查条件分支 |
Retrofit 抛出 HttpException | 直接返回 T 而非 Response<T> | 改用 Response<T> 捕获状态码 |
| Flow 在异常后“死掉” | 未使用 catch 或 retry | 添加 catch 操作符,将异常转为 NetworkResponse.Exception |
| UI 端无法感知 Loading | flow 只发射最终结果 | 使用 onStart 或 map 发射 Loading 状态 |
4.7 性能与稳定性优化
- 超时控制:在 Retrofit 构建器设置
callTimeout、connectTimeout、readTimeout,避免挂起过长。 - 流去重:使用
distinctUntilChanged()避免连续相同 Result 导致 UI 重复刷新。 - Scope 管理:在 ViewModel 中使用
viewModelScope收集 Flow,确保网络请求能在界面销毁时自动取消。 - 缓存策略隔离:将网络数据源封装为独立模块,Flow 转换只做“请求→封装→发射”,缓存逻辑由另一层(如
CacheManager)组合。
// Retrofit 超时配置示例
val okHttpClient = OkHttpClient.Builder()
.connectTimeout(30, TimeUnit.SECONDS)
.readTimeout(30, TimeUnit.SECONDS)
.writeTimeout(30, TimeUnit.SECONDS)
.build()
val retrofit = Retrofit.Builder()
.baseUrl("https://api.example.com/")
.client(okHttpClient)
.addConverterFactory(GsonConverterFactory.create())
.build()
🎯 核心结论: Retrofit 接口 + suspend 函数 +
sealed class NetworkResponse + flow { } 构成网络数据源的标准四件套。这套组合确保了类型安全、状态可枚举、异常不泄露、自然支持重试与轮询,是 Kotlin Flow 缓存更新案例中网络层的基石。