【无标题】

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)
  • 类似模式遍布整个项目:AccountProfileDaoChatMessageDaoGiftDao

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 设计原则

  1. 底层冷流、上层热流:数据层用冷流(按需触发),ViewModel 层转热流(UI 持续观察)
  2. 状态用 StateFlow,事件用 SharedFlow
    • 状态:有当前值、可重读 → StateFlow
    • 事件:一次性、不重播 → SharedFlow (replay=0)
  3. 对内可变、对外只读MutableXxxFlow 私有,对外暴露 XxxFlow / .asStateFlow() / .asSharedFlow()
  4. 生命周期感知 :UI 层用 collectAsStateWithLifecycle 避免泄漏
  5. 共享上游用 stateIn:多个下游消费同一上游时,用 stateIn 共享,避免重复执行
相关推荐
mmsx1 小时前
osmdroid 接入谷歌影像底图:瓦片 URL 拼接与多域名轮询实战
android·osmdroid
ZYJCSZKJ2 小时前
基于大语言模型的地域实体语义增强:生成式引擎优化(GEO)中的多模态内容生成实践
android·人工智能·语言模型
ssshooter2 小时前
Tauri + GitHub Actions 自动化发布 Android APK 技术总结
android·github·android studio
2401_885885042 小时前
国际语音php接口代码示例:PHP使用cURL快速调用语音发送API
android·开发语言·前端·人工智能·python·php·语音识别
_李小白2 小时前
【Android UI开发】Android VideoView 使用介绍(一)
android·ui
Mr数据杨3 小时前
【Codex】用学生支持周报模块跟踪个性化支持进展
android·java·javascript·django·codex·项目开发
名剑走天下3 小时前
Android 面试题大全
android·kotlin
mmsx3 小时前
osmdroid 自定义地图视图:继承 MapView 的正确姿势与生命周期管理
android·osmdroid
ii_best4 小时前
移动开发工具按键精灵安卓版 OcrEx 识别总是漏字错字?改这一个参数,精度直接翻倍
android·ai编程·按键精灵