7、Retrofit + Flow实战:将Retrofit接口返回Flow、使用Flow进行数据转换、结合StateFlow管理UI状态
各位同学,今天我们来聊聊Retrofit和Flow的深度结合。说实话,这个组合是我在项目中最常用的网络层方案。它既保留了Retrofit的简洁,又让数据流变得可观察、可转换。你想想看,以前我们用Call回调,要么嵌套地狱,要么用LiveData还得手动处理生命周期。现在有了Flow,一切都变得丝滑了。
7.1 为什么要把Retrofit接口返回Flow?
我个人习惯,只要涉及网络请求,接口返回值直接写成Flow。为什么呢?因为Flow天生就是为异步数据流设计的。你不需要手动创建LiveData,也不需要担心回调泄漏。Retrofit官方从2.6.0版本开始就支持了,直接声明返回类型为Flow<T>即可。
我在项目中遇到过这样一个场景:用户下拉刷新时,需要重新请求数据并更新UI。如果用Call,你得在onResponse里手动赋值。但用Flow,配合StateFlow,数据变化自动通知UI,省心不少。
7.2 实战:定义返回Flow的Retrofit接口
先看一个最简单的例子。假设我们要获取用户列表,接口定义如下:
interface ApiService {
@GET("users")
fun getUsers(): Flow<List<User>>
}
嗯,这里要注意,Retrofit内部会自动在IO线程执行请求,然后通过Flow发射结果。你不需要手动切换线程,Flow的collect默认就在调用线程。但通常我们会在ViewModel里用flowOn(Dispatchers.IO)来指定。
调用方式也很简单:
viewModelScope.launch {
apiService.getUsers()
.flowOn(Dispatchers.IO)
.collect { users ->
// 更新UI
}
}
我曾经犯过一个错误:直接在collect里做耗时操作,导致UI卡顿。后来才意识到,collect是在收集线程执行的,如果要做复杂转换,记得用map或flowOn。
7.3 使用Flow进行数据转换
Flow的强大之处在于它有一整套操作符。比如我们拿到用户列表后,需要过滤出活跃用户,再映射成UI模型:
apiService.getUsers()
.flowOn(Dispatchers.IO)
.map { users ->
users.filter { it.isActive }
.map { UserUiModel(it.name, it.avatarUrl) }
}
.catch { e ->
emit(emptyList()) // 出错时发射空列表
Log.e("TAG", "请求失败", e)
}
.collect { uiModels ->
// 更新UI
}
这里我用了catch操作符,它是个好习惯。网络请求随时可能失败,catch能让你优雅地处理异常,而不是直接崩溃。我个人习惯在catch里发射一个默认值,或者一个密封类来表示加载状态。
7.4 结合StateFlow管理UI状态
StateFlow是Flow的“状态容器”版本。它总是持有最新的值,并且只会在值变化时通知收集者。这正好适合做UI状态管理。
来看一个完整的ViewModel示例:
class UserViewModel(
private val apiService: ApiService
) : ViewModel() {
private val _uiState = MutableStateFlow<UiState<List<UserUiModel>>>(UiState.Loading)
val uiState: StateFlow<UiState<List<UserUiModel>>> = _uiState.asStateFlow()
init {
loadUsers()
}
private fun loadUsers() {
viewModelScope.launch {
apiService.getUsers()
.flowOn(Dispatchers.IO)
.map { users ->
users.filter { it.isActive }
.map { UserUiModel(it.name, it.avatarUrl) }
}
.catch { e ->
_uiState.value = UiState.Error("网络异常: ${e.message}")
}
.collect { uiModels ->
_uiState.value = UiState.Success(uiModels)
}
}
}
}
sealed class UiState<out T> {
object Loading : UiState<Nothing>()
data class Success<T>(val data: T) : UiState<T>()
data class Error(val message: String) : UiState<Nothing>()
}
看到没?整个流程非常清晰:请求 -> 转换 -> 状态更新。UI层只需要观察uiState,根据不同的状态显示加载中、成功列表或错误提示。
我在项目中用过LiveData做类似的事,但StateFlow有个好处:它天然支持协程,不会像LiveData那样在子线程更新值时报错。而且StateFlow是冷流转热流,适合做状态持有。
7.5 整体架构图
下面这张图展示了Retrofit + Flow + StateFlow的完整数据流:
7.6 避坑指南
最后分享几个我踩过的坑:
- Flow的冷流特性: 每次collect都会重新执行网络请求。如果你只想请求一次,记得用stateIn或shareIn转为热流。
- 取消协程: viewModelScope会在ViewModel销毁时自动取消,但如果你在Activity里用lifecycleScope,记得配合repeatOnLifecycle使用,避免在后台浪费资源。
- 错误处理: 不要只在collect里try-catch,用catch操作符更优雅。而且catch可以重新发射数据,让Flow继续运行。
- 线程切换: flowOn会影响上游操作符,但不会影响下游。如果你在collect里更新UI,确保它在主线程。
我曾经在项目里用Flow做轮询请求,结果忘了加flowOn(Dispatchers.IO),导致主线程被阻塞,界面直接卡死。后来加上了,问题解决。所以,网络请求一定要指定IO线程。