Android 中的 flow
在协程中,flow 是一种可以顺序发射多个值的类型,比如你可以使用 flow 来接收数据库的实时更新,而挂起函数只能返回一个值。
flow 是建立在协程的基础之上的,在概念上,flow 就是一条可以异步计算的数据流,数据流的数据必须是同一个类型的。比如 Flow<Int> 就是发射整形数据的流。
同样是产生一个数据的序列,flow 和 Iterator 很像,但是 flow 使用挂起函数来异步生产和消费数据,这意味着 flow 可以安全地通过执行网络请求来获取数据,而不会阻塞主线程。
flow 中有 3 个核心角色:
- Producer(生产者),生产添加给流的数据。
- Intermediary(中间处理者,可选的),中间处理者可以对流中的每个数据值进行转换、过滤、映射等操作,也可以对整个流本身进行调整(比如限流、合并、错误处理等)。
- Consumer(消费者),消费者是数据流的终点,它消费来自流的数据。
创建一个 flow
看下面的示例,数据源以一个固定的时间间隔循环获取新闻数据,由于挂起函数不能返回多个连续的数据,数据源创建并返回了一个 flow 来实现该需求,这里数据源充当生产者的角色:
// 进行网络请求的接口,fetchLatestNews() 是一个挂起函数
interface NewsApi {
suspend fun fetchLatestNews(): List<ArticleHeadline>
}
class NewsRemoteDataSource(
private val newsApi: NewsApi,
private val refreshIntervalMs: Long = 5000
) {
val latestNews: Flow<List<ArticleHeadline>> = flow {
while(true) {
// 调用挂起函数 fetchLatestNews() 获取新闻
val latestNews = newsApi.fetchLatestNews()
emit(latestNews)
delay(refreshIntervalMs) // 延迟 refreshIntervalMs 秒
}
}
}
flow 构建器运行在协程中,因此,可以直接在 flow 构建器中调用挂起函数,但是这里也有一些限制:
- Flow 是顺序的(sequential)。由于生产者运行在一个协程中,当调用挂起函数的时候,生产者会挂起直到挂起函数返回。在这个例子当中,生产者会挂起直到 fetchLatestNews() 执行完成。这意味着,在前一个值处理完之前,不会 emit 下一个值。
- 在 flow { } 中,emit() 函数必须在原始 协程的上下文(CoroutineContext)中调用。如果你试图通过 withContext() 切换上下文,再调用 emit(),会抛出异常。这种情况下你需要使用 callbackFlow 。
flow 的收集
flow 是冷的(cold),且是懒惰(lazy)的,它的生产者代码 不会自动执行,必须通过调用终端操作符(如 collect、toList、first 等)才会真正启动。
collect 是最常用的终端操作符:
- 它会逐个接收 Flow 发出的每个值;
- 需要在协程中调用(因为它是 suspend 函数);
- 接收一个 Lambda,每次有新值时都会被调用;
示例代码如下:
viewModelScope.launch {
flow.collect { value ->
// 处理每个值
}
}
只有调用 collect,Flow 的生产者(如 flow { ... } 中的代码)才会开始运行。
下面是一个通过实现 ViewModel 来消费 Repository 层的数据的示例:
class LatestNewsViewModel(
private val newsRepository: NewsRepository
) : ViewModel() {
init {
viewModelScope.launch {
// 使用 collect 来触发 flow
newsRepository.favoriteLatestNews.collect { favoriteNews ->
// 更新 UI
}
}
}
}
这里调用 collect 会触发 favoriteLatestNews 这个 Flow 的执行。如果该 Flow 中有 while(true) 循环(如定时刷新),它会持续 emit 数据。当 ViewModel 被销毁时(如 Activity 退出),viewModelScope 会被取消,collect 所在的协程就取消了,整个 Flow 生产者会停止。
Flow 的收集会因为以下原因停止:
- collect 所在的协程被取消了。
- 生产者停止 emit 数据。
冷流的默认行为:每次调用 collect,都会重新执行整个生产者逻辑。如果多个地方同时收集同一个冷流(比如两个 UI 同时订阅 favoriteLatestNews):
- 会发起两次网络请求;
- 各自独立定时刷新(不同步);
- 浪费资源;
解决方法是使用 shareIn 共享 Flow(变“热”)。
Jetpack 中的 flow
Flow 被集成到了许多 Jetpack 库中,并且在 Android 第三方库中很流行,Flow 很适合实时数据更新和无限数据流的场景。
你可以使用 Flow with Room 接收数据库更改的通知,当使用 data access objects (DAO) 时,返回一个 Flow 类型来获取实时更新。
@Dao
abstract class ExampleDao {
@Query("SELECT * FROM Example")
abstract fun getExamples(): Flow<List<Example>>
}
每次当 Example 表有修改的时候,一个新的 List<Example> 就会 emit 出来。
把回调转换成 Flow
许多旧式的 Android API(如 Firebase Firestore、Location API、Sensor API 等)使用回调模式:
firebase.addSnapshotListener { snapshot, error ->
// 处理数据更新
}
但回调难以与协程、Flow 等现代异步编程模型集成。
callbackFlow 的作用就是:把回调 API “包装” 成一个可被协程 collect 的 Flow。
看下面的示例,将 Firestore 监听器转为 Flow:
fun getUserEvents(): Flow<UserEvents> = callbackFlow {
var eventsCollection: CollectionReference? = null
try {
eventsCollection = FirebaseFirestore.getInstance()
.collection("collection")
.document("app")
} catch (e: Throwable) {
close(e) // 初始化失败,关闭 Flow 并抛出异常
}
// 注册 Firestore 回调
val subscription = eventsCollection?.addSnapshotListener { snapshot, _ ->
if (snapshot == null) return@addSnapshotListener
try {
// 使用 trySend 发送数据到 Flow
trySend(snapshot.getEvents())
} catch (e: Throwable) {
// 忽略发送失败(如 Flow 已关闭)
}
}
// 当 Flow 被取消或关闭时,会调用 awaitClose{},自动清理资源
awaitClose { subscription?.remove() }
}
这里通过 callbackFlow { ... } 创建了一个支持回调的 Flow,通过 trySend() 函数从回调线程(非协程上下文)安全地向 Flow 发送数据。
与 flow 构建器不同的是,callbackFlow 可以通过 send() 函数从另一个 CoroutineContext 中 emit 值,或者还可以在非协程上下文通过 trySend() 函数 emit 值。
callbackFlow 内部使用 Channel(通道)来缓冲数据,默认容量为 64 个元素。当使用 send(value) 时,如果 Channel 满了,该函数会挂起,直到 Channel 有空间;使用 trySend(value) 时,如果 Channel 满了,函数不会挂起,直接丢弃该数据并返回 false。
在回调中推荐用 trySend,因为回调通常在主线程或第三方线程,不能挂起。并且如果 Flow 消费太慢,丢弃旧数据往往是可接受的(如 UI 状态)。
StateFlow 和 SharedFlow
StateFlow 和 SharedFlow 也是 Flow APIs,它们被设计用来更高效地处理 向多个消费者(收集者)广播数据 的场景。
StateFlow
StateFlow 是一个状态容器(state-holder),它会自动向所有收集者(collectors,比如 UI)发送当前状态值,以及之后的每一次新状态更新。这使得它非常适合在 Android 应用中管理 UI 状态(例如加载中、成功、错误等)。
它的特点:
- 总是有值(初始值必须提供);
- 只发送最新的状态值(不会重放历史所有值,但会发送当前值给新订阅者);
- 值相等时不会重复发送(通过 == 判断,避免冗余更新);
如何读取当前状态?当前状态值可以通过它的 value 属性读取到。
val stateFlow = MutableStateflow(0)
println(stateFlow.value) // 直接读取当前值,比如 0
如何更新状态?想要更新状态,可以给 MutableStateFlow 类的 value 赋一个新值。
val state = MutableStateFlow("Loading")
state.value = "Success" // ✅ 正确方式:更新状态并自动通知所有收集者
一旦赋值,所有正在收集这个 StateFlow 的协程(比如 UI)会立即收到新值。
接着前面的 Kotlin flows 的例子, StateFlow 可以与 ViewModel 结合(比如下例中的 uiState),作为 LatestNewsViewModel 中的 public 成员暴露出来,这样 View 就可以监听 UI 状态的更新,同时保证屏幕的状态不受 configuration change 的影响。
class LatestNewsViewModel(
private val newsRepository: NewsRepository
) : ViewModel() {
// private 类型的成员,只允许 LatestNewsViewModel 自己修改
private val _uiState = MutableStateFlow(LatestNewsUiState.Success(emptyList()))
// public 类型的成员,只读状态(StateFlow),暴露给 UI 层
val uiState: StateFlow<LatestNewsUiState> = _uiState
init {
// 启动协程监听 Repository 的数据流
viewModelScope.launch {
newsRepository.favoriteLatestNews
.collect { favoriteNews ->
// 更新状态 → 所有 UI 收集者自动收到新值
_uiState.value = LatestNewsUiState.Success(favoriteNews)
}
}
}
}
// Represents different states for the LatestNews screen
sealed class LatestNewsUiState {
data class Success(val news: List<ArticleHeadline>): LatestNewsUiState()
data class Error(val exception: Throwable): LatestNewsUiState()
}
这里 _uiState 是生产者(Producer),负责更新状态。uiState 是消费者接口,UI 通过它监听状态。
StateFlow 是“热流”(hot flow):
- 创建后立即存在并持有值;
- 收集热流不会触发生产者的代码的执行(不像 flow { } 那种冷流);
- StateFlow 会一直活跃在内存中,只有当垃圾回收根(garbage collection root)没有引用它的时候,才会回收它。这意味着,这里只要 ViewModel 没被销毁,StateFlow 就一直存活(即使没有 UI 在监听);
UI 层监听 StateFlow 的代码如下:
class LatestNewsActivity : AppCompatActivity() {
private val latestNewsViewModel = // getViewModel()
override fun onCreate(savedInstanceState: Bundle?) {
...
lifecycleScope.launch {
repeatOnLifecycle(Lifecycle.State.STARTED) {
latestNewsViewModel.uiState.collect { uiState ->
when (uiState) {
is LatestNewsUiState.Success -> showFavoriteNews(uiState.news)
is LatestNewsUiState.Error -> showError(uiState.exception)
}
}
}
}
}
}
如果直接用 lifecycleScope.launch { uiState.collect {...} },即使 Activity 进入后台(STOPPED),协程仍在运行,可能导致更新已销毁的 View(崩溃)或浪费资源,解决方案是使用repeatOnLifecycle(Lifecycle.State.STARTED)。它仅在 Activity/Fragment 可见时(STARTED 或 RESUMED)启动收集,进入 STOPPED 时自动取消协程,重新回到前台时自动重启收集。
StateFlow, Flow, 与 LiveData
StateFlow 与 LiveData 有一些相似点,都是 可观察数据 的容器类,并且在 App 架构中遵循相似的模式,但是需要注意的是,StateFlow 与 LiveData 有下面这些不一样的表现:
- StateFlow 需要给构造函数赋初始值,而 LiveData 不需要;
- 当 UI 组件进入 STOPPED 状态时, LiveData.observe() 会自动取消消费者的注册,而收集 StateFlow 或其他别的流则不会自动停止收集。要实现相同的行为,你需要在 Lifecycle.repeatOnLifecycle 代码块中收集流。
使用 shareIn 操作符让冷流变成热流
冷流(Cold Flow)与热流(Hot Flow)的区别
- 冷流,每次有新的收集者(collector)时,都会重新执行整个生产者逻辑(比如重新发起网络请求、重新监听数据库)。 容易造成资源浪费,不适合共享数据。
- 热流(Hot Flow),生产者只执行一次,多个收集者共享同一个数据流,新收集者可获取最近的数据(replay)。更高效,适合像 UI 状态、实时数据广播等场景。
shareIn 是一个 中间操作符(intermediate operator),用于将一个冷流“发布”为一个共享的热流,使得多个收集者可以复用同一个上游数据源,而不是各自触发一次。
shareIn() 函数需要传递以下参数:
- 一个 CoroutinScope,这个 scope 应该比所有收集者活得更久,以便在需要的时候保持共享流的存活,常见的选择比如:viewModelScope。
- 新收集者能收到“历史”数据的数量。比如:replay = 1,表示新收集者立刻收到最后一个值(类似 StateFlow 的行为);replay = 0,表示新收集者只接收未来的值;replay = 3,表示收到最近 3 个值。
- 何时启动上游生产者,这里控制何时真正开始运行原始冷流的策略。
启动策略包括 3 种:
| 策略 | 行为 |
|---|---|
| SharingStarted.WhileSubscribed() | 仅当有活跃收集者时才启动上游;所有收集者取消后,延迟一段时间(可配置)再停止 |
| SharingStarted.Eagerly | 立即启动上游(即使没人收集),直到 scope 结束 |
| SharingStarted.Lazily | 第一个收集者出现时启动,之后永远不关闭(即使没人收集) |
推荐在大多数 UI 场景使用 WhileSubscribed(),避免后台浪费资源。
拿 Kotlin flows 中的 callbackFlow 举个例子,您可以使用 shareIn 在收集器之间共享从 Firestore 获取到的数据,而不是让每个收集器创建一个新流。
class NewsRemoteDataSource(...,
private val externalScope: CoroutineScope,
) {
val latestNews: Flow<List<ArticleHeadline>> = flow {
// Firestore 请求获取最新的新闻(冷流)
val snapshot = firestore.collection("news").get()
emit(snapshot.toArticles())
}.shareIn(
externalScope,
replay = 1,
started = SharingStarted.WhileSubscribed()
)
}
在这个示例中:
- 多个 ViewModel 或 UI 同时收集 latestNews → 只触发一次 Firestore 请求
- 新收集者加入 → 立刻收到最近一次的新闻列表(replay = 1)
- 所有收集者都取消(比如所有页面离开)→ 共享流暂停(WhileSubscribed)
- 之后又有新收集者 → 重新启动 Firestore 请求
SharedFlow
shareIn 函数返回的是一个 SharedFlow。SharedFlow 也是热流,它会 emit 值给所有收集它的消费者。SharedFlow 是 StateFlow 的“更通用、更灵活(高度可配置)”的版本。
Kotlin 协程官方库中,StateFlow 的实现内部就是基于 SharedFlow 构建的(或至少行为上等价于一个配置好的 SharedFlow)。
StateFlow 与 SharedFlow 的区别如下:
| 特性 | StateFlow | SharedFlow(可配置) |
|---|---|---|
| replay | 固定为 1(总是重放最新值) | 可设为 0、1、N |
| 初始值 | 必须提供(状态必须有当前值) | 可以没有初始值 |
| 缓冲/溢出策略 | 固定为丢弃旧值(conflate) | 可选:SUSPEND、DROP_OLDEST、DROP_LATEST |
| 值相等性 | 自动忽略重复值(value == newValue 则不发射) | 默认不忽略,可自行处理 |
| 用途 | 表示“状态”(如 UI 状态) | 通用广播(事件、消息、状态等) |
不使用 shareIn 也可以创建 SharedFlow,举个例子,你可以用 SharedFlow 来实现一个全局的“刷新信号”机制,定时通知整个 App 同步刷新内容。
在下面的代码片段中,TickHandler 中暴露了一个 SharedFlow,以便其他类知道何时刷新内容。与 “StateFlow” 一样,需要在类中使用 MutableSharedFlow 将值 emit 给流。
class TickHandler(
private val externalScope: CoroutineScope,
private val tickIntervalMs: Long = 5000
) {
// 私有可变流(只在本类内部 emit)
private val _tickFlow = MutableSharedFlow<Unit>(replay = 0)
// 公共只读流(对外暴露,只能 collect)
val tickFlow: SharedFlow<Event<String>> = _tickFlow
init {
// 启动一个协程,每隔 tickIntervalMs 毫秒 emit 一次 Unit
externalScope.launch {
while(true) {
_tickFlow.emit(Unit)
delay(tickIntervalMs)
}
}
}
}
这里 Unit 表示“无实际数据,仅作为信号”(类似 void)。replay = 0,则新订阅者不会收到历史信号。
在 NewsRepository 中监听刷新信号的代码如下:
class NewsRepository(
...,
private val tickHandler: TickHandler,
private val externalScope: CoroutineScope
) {
init {
externalScope.launch {
// Listen for tick updates
tickHandler.tickFlow.collect {
refreshLatestNews()
}
}
}
suspend fun refreshLatestNews() { ... }
...
}
这里每当收到一个 Unit,就调用 refreshLatestNews()。同理,你可以在 UserRepository 中也监听 tickFlow,刷新用户信息和收藏夹。
通过以下方式可以定义 SharedFlow 的行为:
- replay,重发以前发射的值给新的订阅者的次数。
- onBufferOverflow,指定 SharedFlow 缓冲区满了的时候的执行策略,默认值是 BufferOverflow.SUSPEND,会让 emit 挂起,直到有空间。其他值包括 DROP_LATEST(丢弃最新的值) 和 DROP_OLDEST(丢弃最旧的值) 。
MutableSharedFlow 还有一个 subscriptionCount 属性,可以知道当前有多少个活跃的收集者,以便你可以相应地优化业务逻辑。比如,如果没人监听,就暂停 tick 发射,节省资源。
externalScope.launch {
if (_tickFlow.subscriptionCount.value > 0) {
// 才开始发射
}
}
MutableSharedFlow 里面还有一个 resetReplayCache 函数,可以使用它手动清空重放缓存。比如用户登出时,不想让新登录的用户看到旧的状态。
参考文档:
developer.android.com/kotlin/flow developer.android.com/kotlin/flow…