Kotlin Flow 在 Aiban 工程中的使用总结
一、核心概念
冷流 (Cold Flow)
- 定义:只有被收集(collect)时才开始执行,每个收集者都会触发独立的数据流执行
- 特点:按需生产、多订阅者独立、无状态
- 典型场景:数据库查询、网络请求回调封装、DataStore 读取
热流 (Hot Flow)
- 定义:无论是否有收集者,数据流都在活跃地发射数据
- 特点:主动发射、多订阅者共享、有状态/无状态之分
- 分类 :
- StateFlow:有状态,始终持有当前值,新订阅者立即收到最新值
- SharedFlow:可配置 replay(重播)、缓冲大小、溢出策略
二、冷流 Flow 的使用
2.1 callbackFlow --- 封装回调式 API
原理 :将基于回调的传统 API 转换为 Flow。使用 trySend 发送数据,awaitClose 注册取消回调。
实例:网络状态监听
ConnectivityHelperImpl.kt
kotlin
override fun connectivityStatusFlow(): Flow<ConnectivityStatus> =
callbackFlow {
val callback = object : ConnectivityManager.NetworkCallback() {
override fun onAvailable(network: Network) {
trySend(getConnectivityStatus())
}
override fun onLost(network: Network) {
trySend(getConnectivityStatus())
}
override fun onCapabilitiesChanged(
network: Network,
networkCapabilities: NetworkCapabilities,
) {
trySend(getConnectivityStatus())
}
}
val networkRequest = NetworkRequest.Builder()
.addCapability(NetworkCapabilities.NET_CAPABILITY_INTERNET)
.build()
connectivityManager.registerNetworkCallback(networkRequest, callback)
trySend(getConnectivityStatus()) // 立即发射当前状态
awaitClose {
connectivityManager.unregisterNetworkCallback(callback)
}
}.distinctUntilChanged()
要点:
trySend()非挂起发送,适合回调线程awaitClose { }在 Flow 被取消时清理资源.distinctUntilChanged()去重,避免相同状态重复下发
2.2 Room DAO 返回 Flow --- 数据库响应式查询
原理:Room 自动监听表变化,数据变更时自动发射新值。数据库变化 → Flow 自动发射新列表。
实例:会话列表查询
ConversationDao.kt
kotlin
@Dao
internal interface ConversationDao {
@Query("SELECT * FROM conversation_table ORDER BY last_msg_time DESC")
fun observeAllConversations(): Flow<List<ConversationEntity>>
@Query("SELECT * FROM conversation_table WHERE partner_id = :partnerId")
fun getConversation(partnerId: Int): Flow<ConversationEntity?>
}
Repository 层映射:
kotlin
override fun observeAntiFraudNotifyMessages(
fromUid: Int,
targetId: Int,
chatType: ChatType,
): Flow<List<IMChatMessage>> =
localDatasource.observeAntiFraudNotifyMessages(fromUid, targetId, chatType.value)
.map { entities ->
entities.map { chatMessageLocalToDomainMapper(it) }
}
要点:
- 返回
Flow<T>的 DAO 方法是冷流,collect 时才开始查询 - 数据库表数据变化时自动重新查询并发射新值
- 通过
.map { }进行数据模型转换(Entity → Domain) - 类似模式遍布整个项目:
AccountProfileDao、ChatMessageDao、GiftDao等
2.3 DataStore Flow --- 键值对响应式读取
原理 :DataStore 的 .data 属性是一个 Flow,数据变更时自动发射。
实例:用户凭证与偏好存储
AccountLocalDatasourceImpl.kt
kotlin
// Proto DataStore
override fun myCredentialsFlow(): Flow<UserCredentialsEntity?> =
userCredentialsDataStore.data
// Preferences DataStore
override fun hasAgreedProtocolFlow(): Flow<Boolean> =
globalPreferenceDataStore.data.map { it[hasAgreedProtocol] == true }
override fun observeLastVerificationSentTime(): Flow<Long> =
globalPreferenceDataStore.data.map { it[verificationCodeSentTime] ?: 0 }
一次性读取(冷流转挂起函数):
kotlin
override suspend fun getMyCredentials(): UserCredentialsEntity? =
userCredentialsDataStore.data.firstOrNull()
要点:
dataStore.data返回 Flow,数据变化时自动推送.firstOrNull()只取第一个值然后结束,用于一次性读取- 通过
.map { }从 Preferences 中提取具体字段
2.4 flow { } 构建器 --- 自定义冷流
实例:
CreateRoomUseCase.kt
kotlin
class CreateRoomUseCase(
private val userProfilesRepository: UserProfilesRepository,
private val imRepository: IMRepository,
) : UseCase<Int, Flow<Any>> {
override suspend fun invoke(params: Int?): Flow<Any> {
val currentUserId = userProfilesRepository.getCurrentUserId().getOrNull()
?: throw IllegalStateException("当前用户未登录")
return flow {
// 可在此 emit 多个值
}
}
}
要点:
flow { }中使用emit()发射值- 每次 collect 都会重新执行 block 中的代码
三、StateFlow 的使用
3.1 核心原理
- 有状态热流:始终持有一个当前值(value 属性)
- 新订阅者立即收到最新值:collect 时先发当前值,再发后续更新
- 值的不可变性 :通过
value = newValue更新,内部做相等性检查(distinctUntilChanged) - conflate 特性:快速连续更新时,中间值可能被合并,收集者只收到最新值
3.2 UI 状态管理(BaseViewModel 模式)
核心实现:
BaseViewModel.kt
kotlin
// 内部可变的 UI 状态流
private val mutableUIStateFlow = MutableStateFlow<UIState>(UIState.Idle)
// 对外暴露只读 StateFlow
val uiStateFlow: StateFlow<UIState> = mutableUIStateFlow
// 发射新状态
protected suspend fun emitUIState(state: UIState) {
if (state is UIState.Data<*>) {
mutableLastDataFlow.value = state
}
mutableUIStateFlow.emit(state)
}
解决 StateFlow conflate 问题的 lastDataFlow:
kotlin
// 仅存储最近一次 UIState.Data,不被 Idle/Loading 覆盖
private val mutableLastDataFlow = MutableStateFlow<UIState.Data<*>?>(null)
val lastDataFlow: StateFlow<UIState.Data<*>?> = mutableLastDataFlow
问题背景 :StateFlow 会合并(conflate)快速连续的发射。当 UIState.Data 之后紧跟 UIState.Idle 时,Compose 收集器可能跳过 Data 状态,导致界面拿不到数据。
解决方案 :用独立的 lastDataFlow 专门保存数据,只在 UIState.Data 时更新,永远不会被 Idle/Loading 重置。
3.3 ViewModel 中的业务 StateFlow
实例:IM 首页会话列表
IMHomeViewModel.kt
kotlin
// 将 Repository 的冷流转换为热的 StateFlow,共享给多个下游
private val sharedConversationsFlow: StateFlow<List<IMConversation>> =
imRepository
.conversationFlow()
.stateIn(
scope = viewModelScope,
started = SharingStarted.WhileSubscribed(stopTimeoutMillis = 60_000L),
initialValue = emptyList(),
)
stateIn 三个参数的含义:
scope:共享协程作用域started = WhileSubscribed(60_000):没有订阅者后延迟 60 秒再停止上游,避免配置重建时重新收集initialValue:初始值
与 flatMapLatest 组合使用:
kotlin
sharedConversationsFlow
.flatMapLatest { conversations ->
val partnerIds = conversations.map { it.partnerId }.toSet()
if (partnerIds.isEmpty()) {
flowOf(conversations to emptyList<AccountProfile>())
} else {
userProfilesRepository
.accountProfilesFlow(partnerIds)
.map { profiles -> conversations to profiles }
}
}.collect { (conversations, profiles) ->
// 更新 UI 状态
emitUIState(UIState.Data(IMHomeViewModelUIState(...)))
}
要点:
stateIn将冷流转换为热流,实现多订阅者共享WhileSubscribed(60_000)防止配置变更导致的重新收集flatMapLatest实现"会话列表变化 → 重新查询对应的用户资料"的联动
3.4 连接状态 StateFlow
实例:WebSocket 连接状态
IMWebSocketClientImpl.kt
kotlin
private val _stateFlow = MutableStateFlow(ConnectionState.DISCONNECTED)
override fun connectionStateFlow(): Flow<ConnectionState> = _stateFlow
// 连接中
_stateFlow.value = ConnectionState.CONNECTING
// 连接成功
_stateFlow.value = ConnectionState.CONNECTED
// 连接失败
_stateFlow.value = ConnectionState.FAILED
要点:
- 用
.value =直接赋值更新(线程安全) - 适合表示"当前状态"类的连续数据
- 新订阅者立即知道当前连接状态
3.5 内存状态存储(LiveRoomSessionStore)
实例:直播间会话状态
LiveRoomSessionStore.kt
kotlin
// 按 roomId 动态创建 StateFlow
private val sessions = ConcurrentHashMap<Int, MutableStateFlow<LiveRoomSession?>>()
fun observeSession(roomId: Int): StateFlow<LiveRoomSession?> =
sessions.computeIfAbsent(roomId) { MutableStateFlow(null) }.asStateFlow()
// 使用 update 原子更新
fun applyUserJoined(roomId: Int, member: RoomMember): Boolean {
updateMembers(roomId) { snapshot ->
LiveRoomBroadcastReducer.applyUserJoined(snapshot, member)
}
// ...
}
private inline fun updateMembers(
roomId: Int,
crossinline transform: (RoomMembersSnapshot) -> RoomMembersSnapshot,
) {
val flow = sessions.computeIfAbsent(roomId) { MutableStateFlow(null) }
flow.update { current ->
val base = current ?: LiveRoomSession(roomId = roomId)
base.copy(members = transform(base.members))
}
}
要点:
ConcurrentHashMap+computeIfAbsent实现动态 key → StateFlow 映射.update { }原子更新,避免竞态条件- 适合内存中的领域状态管理
四、SharedFlow 的使用
4.1 核心原理
- 可配置热流:通过 replay、extraBufferCapacity、onBufferOverflow 三个参数定制行为
- replay:新订阅者能收到之前多少个历史值
- extraBufferCapacity:额外缓冲容量(无订阅者时缓存)
- onBufferOverflow:缓冲溢出策略(SUSPEND / DROP_OLDEST / DROP_LATEST)
4.2 一次性 UI 事件(replay = 0)
核心实现 --- BaseViewModel:
BaseViewModel.kt
kotlin
// replay=0 保证事件不会在重订阅时重复触发
private val mutableUIEventFlow = MutableSharedFlow<UIEvent>(replay = 0)
val uiEventFlow: SharedFlow<UIEvent> = mutableUIEventFlow
protected suspend fun emitUIEvent(event: UIEvent) {
mutableUIEventFlow.emit(event)
}
UI 层收集方式(AibanScaffold 中):
AibanScaffold.kt
kotlin
// 使用 LaunchedEffect 直接收集,不用 collectAsStateWithLifecycle
// 原因:防止等值事件被 state 过滤掉
LaunchedEffect(viewModel.uiEventFlow) {
viewModel.uiEventFlow.collect { uiEvent ->
if (uiEvent is MessageEvent) {
when (uiEvent.presentedBy) {
PresentationMethod.Snackbar -> {
snackbarHostState.showSnackbar(...)
}
PresentationMethod.Dialog -> {
dialogMsg = messageText
}
}
}
}
}
要点:
replay = 0:新订阅者不会收到历史事件(一次性事件语义)- 用
LaunchedEffect + collect而非collectAsStateWithLifecycle:- 后者会用 State 包装,相同值会被去重,导致"相同文案的两次 Toast 只显示一次"
- 前者直接收集,每个事件都处理
4.3 消息广播流(带缓冲)
实例:WebSocket 消息流
IMWebSocketClientImpl.kt
kotlin
// 业务消息流(带缓冲,避免无订阅者时挂起)
private val _messageFlow = MutableSharedFlow<ChatPacket>(extraBufferCapacity = 64)
// 内部异常上报流
private val _errorFlow = MutableSharedFlow<Throwable>(extraBufferCapacity = 16)
// 客户端视角 RTT 流
private val _heartbeatLatencyFlow = MutableSharedFlow<Long>(extraBufferCapacity = 16)
消息发送:
kotlin
private suspend fun handleFrame(frame: Frame) {
when (frame) {
is Frame.Text -> {
val packet = json.decodeFromString(ChatPacket.serializer(), text)
if (packet.signal == "pong") {
_heartbeatLatencyFlow.emit(rtt) // RTT 延迟
} else {
_messageFlow.emit(packet) // 业务消息
}
}
}
}
要点:
extraBufferCapacity = 64:即使暂时没有订阅者,消息也能缓冲 64 条不挂起- 无 replay:新订阅者收不到历史消息,只收到订阅后的新消息
- 适合"推送式"的消息流场景
4.4 事件总线(带 replay 的 Feed 流)
实例:直播间公屏 Feed
LiveRoomSessionStore.kt
kotlin
/**
* 公屏 Feed。必须 replay > 0:
* user_joined 常在 ViewModel collect 之前到达;
* 仅 extraBuffer、无订阅者时事件会丢,迟到的 collect 永远收不到。
*/
private val _feedEvents = MutableSharedFlow<LiveRoomFeedEvent>(
replay = 32,
extraBufferCapacity = 32,
onBufferOverflow = BufferOverflow.DROP_OLDEST,
)
麦位通知流(replay = 16):
kotlin
private val _micNotifications = MutableSharedFlow<MicNotification>(
replay = 16,
extraBufferCapacity = 16,
onBufferOverflow = BufferOverflow.DROP_OLDEST,
)
普通通知流(replay = 0):
kotlin
private val _matchmakerNotifications = MutableSharedFlow<MatchmakerNotification>(
extraBufferCapacity = 16
)
private val _guestNotifications = MutableSharedFlow<GuestNotification>(
extraBufferCapacity = 16
)
要点:
- replay > 0:解决"消息先到、订阅后到"的时序问题(晚到的订阅者也能收到历史消息)
- BufferOverflow.DROP_OLDEST:溢出时丢最旧的,保留最新的
- 不同场景选择不同 replay 值:
- 公屏 Feed:replay=32(需要看到历史消息)
- 麦位通知:replay=16(麦位状态需要一定历史)
- 红娘通知:replay=0(一次性通知,不需要历史)
4.5 跨 ViewModel 事件总线
实例:相亲首页事件总线
MarriageHomeEventBus.kt
kotlin
class MarriageHomeEventBus {
private val _events = MutableSharedFlow<MarriageHomeUIEvent>(
replay = 0,
extraBufferCapacity = 64
)
val events: SharedFlow<MarriageHomeUIEvent> = _events.asSharedFlow()
suspend fun emit(event: MarriageHomeUIEvent) {
_events.emit(event)
}
}
要点:
- 独立的事件总线类,通过 Koin 注入为 single
- 用于同一页面内多个 ViewModel / 组件间通信
- replay=0:一次性事件语义
4.6 Repository 层事件通知
实例:聊天卡片不足 / 实名认证提醒
ChatRepositoryImpl.kt
kotlin
private val _chatCardInsufficientFlow = MutableSharedFlow<Unit>(extraBufferCapacity = 1)
override fun chatCardInsufficientFlow(): Flow<Unit> = _chatCardInsufficientFlow
private val _realNameAuthRequiredFlow = MutableSharedFlow<Unit>(extraBufferCapacity = 1)
override fun realNameAuthRequiredFlow(): Flow<Unit> = _realNameAuthRequiredFlow
要点:
- Repository 层向上层发出"触发式"事件
extraBufferCapacity = 1:保证事件不丢失,但只缓冲 1 个- 上层(ViewModel)collect 后触发相应 UI 提示
五、UI 层收集 Flow 的方式
5.1 collectAsStateWithLifecycle --- StateFlow 转 Compose State
适用场景:StateFlow / 需要生命周期感知的流
kotlin
// 收集网络状态
val isConnected by connectivityHelper.isConnectedFlow()
.collectAsStateWithLifecycle(initialValue = null)
// 收集 UI 状态
val uiState by viewModel.uiStateFlow.collectAsStateWithLifecycle()
// 收集认证状态
val authStatus by accountRepo.authStatusFlow()
.collectAsStateWithLifecycle(initialValue = null)
原理:
- 具有生命周期感知,页面 onStop 时自动停止收集,onStart 时恢复
- 避免内存泄漏和不必要的资源消耗
- 配合
by关键字委托,直接当做 Compose State 使用
5.2 LaunchedEffect + collect --- SharedFlow 一次性事件
适用场景:SharedFlow 一次性事件(Toast、Dialog、导航)
kotlin
LaunchedEffect(viewModel.uiEventFlow) {
viewModel.uiEventFlow.collect { uiEvent ->
// 处理每个事件
when (uiEvent.presentedBy) {
PresentationMethod.Snackbar -> { /* 显示 Snackbar */ }
PresentationMethod.Dialog -> { /* 显示 Dialog */ }
}
}
}
为什么不用 collectAsStateWithLifecycle:
collectAsStateWithLifecycle内部用 State 包装,相同值会被去重- 两个相同文案的 Toast 连续发出时,第二个会被当成"相同状态"而忽略
- 直接
collect保证每个事件都被处理
5.3 first / firstOrNull --- 一次性取值
适用场景:只需要当前值,不需要持续观察
kotlin
// 一次性读取用户凭证
override suspend fun getMyCredentials(): UserCredentialsEntity? =
userCredentialsDataStore.data.firstOrNull()
// 一次性读取账号资料
override suspend fun getAccountProfile(profileId: Int): AccountProfileEntity? =
accountProfileDao.getById(id = profileId).firstOrNull()
要点:
first()取第一个值后 Flow 结束firstOrNull()空安全版本- 适合"取一次就走"的场景
六、Flow 操作符使用汇总
| 操作符 | 用途 | 项目中的典型场景 |
|---|---|---|
.map { } |
数据转换 | Entity → Domain 模型映射 |
.distinctUntilChanged() |
去重 | 网络状态变化去重 |
.stateIn() |
冷流转热 StateFlow | 会话列表共享 |
.shareIn() |
冷流转热 SharedFlow | --- |
.flatMapLatest() |
流的切换联动 | 会话变化 → 查询用户资料 |
.firstOrNull() |
取第一个值 | 一次性读取 DataStore / DB |
.asStateFlow() |
只读化包装 | 对外暴露不可变 StateFlow |
.asSharedFlow() |
只读化包装 | 对外暴露不可变 SharedFlow |
七、架构模式总结
7.1 分层 Flow 流向图
Data Layer (冷流为主)
├── Room DAO Flow ← 数据库表变化自动触发
├── DataStore Flow ← 偏好数据变化自动触发
├── callbackFlow ← 系统回调封装(网络状态等)
└── WebSocket SharedFlow ← 消息推送(热流,带缓冲)
↓
Repository Layer (冷流 + 热流混合)
├── 观察类方法返回 Flow ← 透传或 map 转换
├── 事件通知 SharedFlow ← 业务事件向上通知
└── stateIn/shareIn ← 冷流转热流共享
↓
ViewModel Layer (StateFlow 为主)
├── uiStateFlow (StateFlow) ← UI 状态
├── lastDataFlow (StateFlow) ← 最近数据(解决 conflate 问题)
├── uiEventFlow (SharedFlow) ← 一次性事件
└── 业务 StateFlow ← 组合多个数据源
↓
UI Layer (Compose)
├── collectAsStateWithLifecycle ← StateFlow → Compose State
└── LaunchedEffect + collect ← SharedFlow 一次性事件
7.2 设计原则
- 底层冷流、上层热流:数据层用冷流(按需触发),ViewModel 层转热流(UI 持续观察)
- 状态用 StateFlow,事件用 SharedFlow :
- 状态:有当前值、可重读 → StateFlow
- 事件:一次性、不重播 → SharedFlow (replay=0)
- 对内可变、对外只读 :
MutableXxxFlow私有,对外暴露XxxFlow/.asStateFlow()/.asSharedFlow() - 生命周期感知 :UI 层用
collectAsStateWithLifecycle避免泄漏 - 共享上游用 stateIn:多个下游消费同一上游时,用 stateIn 共享,避免重复执行