6、Flow基础:Flow是什么、冷流与热流的区别、Flow的构建与终端操作符

好,咱们今天来聊聊 Flow。说实话,刚接触协程那会儿,我对 Flow 是有点抵触的——「不就是个异步序列吗?我用 LiveData 加个线程池不也能搞定?」后来在项目里踩了几次坑,才真正体会到 Flow 的设计有多巧妙。

Flow 是什么?说白了,它是一个冷数据流。你可以把它想象成一条水管,数据像水流一样从上游流下来,下游有人打开水龙头(collect),水才会开始流。没人接水,水管就是空的。

冷流与热流的区别

这个区别我当年理解了好久。咱们直接看对比:

特性 冷流(Flow) 热流(StateFlow / SharedFlow)
数据生产时机 有人订阅才开始 不管有没有订阅,都在生产
每个订阅者 收到完整数据序列 可能只收到订阅之后的数据
典型场景 网络请求、数据库查询 UI 状态、事件总线
是否共享 不共享,每个订阅者独立 共享,多个订阅者共用

我举个例子你就明白了。冷流就像 Netflix——你点开一部电影,它才开始下载播放。热流就像电视台——不管你看不看,节目一直在播。你打开电视只能看到当前正在播的内容,看不到之前的。

核心要点:Flow 默认是冷流。每个终端操作符(比如 collect)都会触发一次全新的数据生产。同一个 Flow 被 collect 两次,数据会生产两次。

我个人习惯:网络请求用冷流,UI 状态用热流。别搞混了,否则会出现「请求发了两次」或者「状态丢失」的诡异 bug。

Flow 的三种构建方式

Kotlin 给我们提供了三种最常用的构建方式,我按使用频率排个序:

1. flowOf

最简单的方式,适合已知的固定数据:

val numberFlow: Flow<Int> = flowOf(1, 2, 3, 4, 5)

嗯,就这么简单。你传什么,它就发什么。我在写单元测试时经常用这个,模拟一个有限的数据流。

2. asFlow

把现有集合或序列转成 Flow:

val listFlow: Flow<Int> = listOf(1, 2, 3).asFlow()
val arrayFlow: Flow<Int> = intArrayOf(1, 2, 3).asFlow()

这个我用的最多的地方是——把数据库返回的 List 转成 Flow,然后做链式操作。你想想看,本来要写一堆 for 循环,现在一个 map 就搞定了。

3. flow {} 构建器

最灵活的方式,可以发射任意数据,甚至异步数据:

val userFlow: Flow<User> = flow {
    // 发射第一个数据
    emit(fetchUserFromCache())
    
    // 模拟网络请求
    val user = fetchUserFromNetwork()
    emit(user)
    
    // 可以发射多个
    for (i in 1..3) {
        emit(updateUserStatus(i))
        delay(1000) // 注意:这里可以挂起
    }
}

我曾经踩过的坑:在 flow {} 里直接调用了阻塞式 IO 操作,比如 File.readText()。结果 collect 的时候卡住了主线程。记住:flow {} 的代码块是挂起上下文,但如果你在里面调了阻塞方法,它不会自动帮你切线程。需要配合 flowOn(Dispatchers.IO) 使用。

Flow 的终端操作符

终端操作符,就是触发数据流动的「开关」。没有它们,Flow 就是个摆设。

collect

最常用的终端操作符,没有之一:

lifecycleScope.launch {
    flowOf(1, 2, 3).collect { value ->
        Log.d("Flow", "收到: $value")
    }
}

collect 是一个挂起函数,它会一直阻塞直到 Flow 结束(或者协程被取消)。

toList

把 Flow 的所有数据收集到一个 List 中:

val result: List<Int> = flowOf(1, 2, 3).toList()
// result = [1, 2, 3]

这个在测试里特别好用。我经常用它来验证 Flow 发射的数据是否符合预期:

@Test
fun `test user flow`() = runTest {
    val flow = userRepository.getUsers()
    val users = flow.toList()
    assertEquals(3, users.size)
}

first

只取第一个数据,然后取消 Flow:

val firstNumber: Int = flowOf(1, 2, 3).first()
// firstNumber = 1

也可以带条件:

val firstEven: Int = flowOf(1, 2, 3, 4).first { it % 2 == 0 }
// firstEven = 2

这个操作符我经常用在「只需要最新状态」的场景。比如用户登录后,我只想拿一次用户信息,不需要持续监听。

知识体系总览

下面这张图帮你理清 Flow 的核心脉络:

Flow 核心知识体系 Flow 冷流(默认) 热流(StateFlow等) 订阅后才生产 每个订阅者独立 持续生产数据 订阅者共享 三种构建方式 flowOf() asFlow() flow {} 终端操作符 collect toList first

这张图把 Flow 的三个核心维度串起来了。你从上往下看:先搞清楚冷热流,再掌握构建方式,最后用终端操作符触发执行。这三步走完,Flow 的基础就算打牢了。

我个人建议:刚开始学 Flow,先死磕 flow {}collect 这两个。其他操作符都是锦上添花。把这两个用熟了,后面学 map、filter、catch 这些中间操作符会轻松很多。

好了,Flow 的基础就这些。下一节咱们会深入中间操作符,到时候你会发现——原来 Flow 的链式调用这么爽。


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