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,省心不少。

核心优势: Flow + Retrofit = 声明式网络请求 + 响应式数据流

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里发射一个默认值,或者一个密封类来表示加载状态。

小技巧: 如果多个接口需要组合,可以用zip或combine操作符。比如同时请求用户信息和配置信息,等两者都返回后再合并。

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是冷流转热流,适合做状态持有。

注意: StateFlow的value更新必须在主线程吗?不一定。但如果你在子线程更新,UI层观察时可能需要切换线程。建议统一在主线程更新状态,或者使用stateIn操作符指定线程。

7.5 整体架构图

下面这张图展示了Retrofit + Flow + StateFlow的完整数据流:

Retrofit + Flow + StateFlow 数据流架构 Retrofit 接口 返回 Flow<T> Flow 操作符 map / filter / catch StateFlow 持有UI状态 UI 层 (Activity/Fragment) collect / observe ViewModel viewModelScope.launch 数据从Retrofit流出,经过Flow转换,最终通过StateFlow驱动UI 网络请求 数据转换 状态管理 UI渲染

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线程。

总结: Retrofit + Flow + StateFlow 是Android现代网络层的黄金组合。它让代码更简洁、数据流更清晰、状态管理更安全。如果你还没用上,建议下次项目试试。

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