到目前为止,我们一直在讨论冷流(我们的空气压缩机)。提醒一下:冷流在有人拉动触发器(.collect())之前绝对什么都不做。如果有三个人拉动触发器,压缩机就会启动三次,每次都从零开始做完全相同的工作。
但如果你不想要那样呢?如果你想要一个总是运行的中心真实来源,并且多个人都可以查看它呢?
欢迎来到热流(Hot Flows)。在 Android 中,我们使用两种主要类型的热流:StateFlow 和 SharedFlow。
分解:广告牌 vs. 无线电塔
1. StateFlow:体育场广告牌(用于 STATE)
想象你在一场足球比赛中。广告牌就是一个 StateFlow。
- 它必须始终有一个值。比赛从 0-0(初始状态)开始。
- 如果你去洗手间然后回来,你不会关心每一块记分牌,你只关心你不在时的最新得分。
- 如果比分是 7-0,而记分牌操作员试图再次将其更新为 7-0,什么也不会发生。它会忽略连续的重复值。
代码示例(ViewModel 中):
// 1. 私有可变版本(操作记分牌的人)
private val _uiState = MutableStateFlow("Loading...")
// 2. 公共只读版本(看广告牌的粉丝)
val uiState: StateFlow = _uiState.asStateFlow()
fun finishLoading() {
_uiState.value = "Welcome to the App!" // 更新广告牌
}2. SharedFlow:无线电塔(用于 EVENTS)
想象一个广播电台正在直播一场音乐会。这就是 SharedFlow。
- 它不需要初始值。
- 如果你在下午 5:00 调频收听,你会听到现在正在播放的任何内容。
- 如果你在下午 4:55 错过了一首歌,那首歌就永远消失了(除非电台有重播缓存,而 SharedFlow 实际上支持!)。
- 它非常适合一次性事件:显示 Toast 消息、播放声音或导航到新屏幕。你不想仅仅因为用户旋转了手机就导航到新屏幕。
代码示例(ViewModel 中):
// 4. 没有重播缓存的 SharedFlow。如果 UI 没有在监听,事件就会丢失
private val _navigationEvent = MutableSharedFlow()
val navigationEvent: SharedFlow = _navigationEvent.asSharedFlow()
suspend fun triggerNavigation() {
_navigationEvent.emit("NavigateToDetails") // 向所有监听者广播
}图表化思维模型:
STATEFLOW(记分牌):
[初始: 0] -> [得分: 1] -> [得分: 2] -> (粉丝观看) -> 看到: 2
(始终保存绝对最新的状态)
SHAREDFLOW(无线电塔):
[歌曲 1] -> [歌曲 2] -> (用户调频进入) -> [歌曲 3] -> 看到: 3
(用户错过了歌曲 1 和 2,因为他们当时没在听!)“等等,为什么?”环节
疑问:“为什么我们在 ViewModel 中要搞这种 private _uiState 和 public uiState 的舞蹈?为什么不直接把 MutableStateFlow 设为公共的?”
回答:这关乎信任和边界!如果你把 MutableStateFlow 设为公共的,任何 Fragment 或 Activity 都可以随意将其数据更改为他们想要的任何内容。你的 UI 应该是愚蠢的——大脑。通过暴露 .asStateFlow(),你给 UI 提供了一个只读菜单。他们可以看,但不能改价格。
疑问:“好吧,StateFlow 听起来完全像 LiveData。我们为什么要弃用 LiveData?”
回答:你说得对,它们的行为非常相似!然而,LiveData 与 Android 框架深度绑定(它需要 Android 的 Looper 和主线程)。这使得它很难在纯 Kotlin 模块(如 Domain 和 Main 层)中使用,并且难以进行单元测试。StateFlow 是纯 Kotlin。它没有 Android 的概念,这使你的架构更加干净。此外,StateFlow 与我们第2部分中学到的所有很棒的运算符(map、combine、filter)无缝集成!
分层面试问题(预期内容)
Level 1:初级开发者(1-3年经验)
- 问题:StateFlow 和 SharedFlow 之间的主要区别是什么?
- 如何回答:“StateFlow 设计用于保存状态。它需要初始值,只保存单个最新状态,并合并(忽略)连续的重复值。SharedFlow 设计用于事件。它不需要初始值,你可以配置它来重播多个过去的值给新订阅者,并且它发出每一个单一事件,即使它们是重复的。”
Level 2:中级开发者(4-5年经验)
- 问题:如果我在 Activity 没有处于活动状态时使用 SharedFlow 发射导航事件,会发生什么?
- 如何回答:“如果你的 SharedFlow 配置了默认的 replay=0,该事件会被丢弃并永远丢失。当 Activity 回到前台并再次开始收集时,该事件已经过去了。如果你需要确保事件被捕获,你需要使用一个 Channel,或者配置一个重播缓存,尽管用重播缓存管理已消费的事件可能会变得棘手。”
Level 3:高级/架构师
- 问题:你如何将冷流(如数据库查询)转换为热流?在 ViewModel 中使用
stateIn或shareIn操作符是什么? - 如何回答:“你使用
stateIn或shareIn操作符。这对于避免不必要的数据库读取或网络调用至关重要。SharingStarted.WhileSubscribed(5000)告诉上游流在最后一个 UI 收集器消失后(比如在配置更改或设备旋转期间)保持活动状态 5 秒钟。如果 UI 在 5 秒内重新连接,上游流永远不会重新启动,从而节省资源。如果用户实际离开了应用,5 秒后,上游流会关闭以节省电池和内存。”