ViewModel中多Flow更新DevicesUIState的优化问询
问题描述
我需要在ViewModel中实现一个由多个Flow更新的状态,State类定义如下:
data class DevicesUIState( val currentGroup: GroupEntity? = null, val currentGroupSubGroups: List<GroupEntity> = listOf(), val currentGroupSubDevices: List<DeviceEntity> = listOf(), val allDevices: List<DeviceEntity> = listOf(), )
各属性更新规则:
- currentGroup与allDevices的Flow无需互等即可发射结果
- currentGroupSubGroups和currentGroupSubDevices的Flow依赖currentGroup的值
- 所有Flow均从Room数据库收集数据
当前实现代码如下:
val groupId: Int? val uiState: StateFlow<DevicesUIState> = groupsRepository.groupFlow(groupId) .mapLatest { Timber.e("Mapping currentGroup") DevicesUIState(currentGroup = it) }.flatMapLatest { uiState -> Timber.e("Mapping currentGroupSubGroups") groupsRepository.subGroupsFlow(uiState.currentGroup?.id).map { uiState.copy(currentGroupSubGroups = it) } }.flatMapLatest { uiState -> Timber.e("Mapping currentGroupSubDevices") liteDeviceRepository.subDevicesFlow(uiState.currentGroup?.id).map { uiState.copy(currentGroupSubDevices = it) } }.flatMapLatest { uiState -> Timber.e("Mapping allDevices") liteDeviceRepository.allDevicesFlow().map { uiState.copy(allDevices = it) } }.stateIn( scope = viewModelScope, started = SharingStarted.WhileSubscribed(5_000L), initialValue = DevicesUIState() )
代码可正常运行,但日志显示Flow收集次数远超预期,日志信息如下:
I --> GET https://example.com/api/groups E Mapping currentGroup E Mapping currentGroupSubGroups E Mapping currentGroupSubDevices E Mapping allDevices I <-- 200 https://example.com/api/groups (393ms, unknown-length body) E Mapping currentGroup E Mapping currentGroupSubGroups E Mapping currentGroupSubDevices E Mapping allDevices I --> GET https://example.com/api/devices I <-- 200 https://example.com/api/devices (85ms, unknown-length body) E Mapping allDevices I --> GET https://example.com/api/extraDevicesData I <-- 200 https://example.com/api/extraDevicesData (258ms, unknown-length body) E Mapping allDevices
寻求正确的实现方案。
问题分析与解决方案
问题原因
当前链式flatMapLatest的实现存在两个核心问题:
- currentGroup更新触发全链路重走:每次currentGroup的Flow发射新值,会依次触发subGroups、subDevices、allDevices的Flow重新订阅和映射,即使allDevices与currentGroup无关,也会被重复触发。
- allDevices无法独立更新状态:allDevices的Flow被嵌套在最后一层,只能依赖前面链路传递的状态进行合并,无法独立更新UIState,导致不必要的流程联动。
正确实现方式
采用combine操作符合并多个Flow,让独立Flow和依赖Flow各自维护,仅在任意Flow有新值时,结合所有Flow的最新状态生成UIState:
val groupId: Int? val uiState: StateFlow<DevicesUIState> = combine( // 基础currentGroup流 groupsRepository.groupFlow(groupId), // 依赖currentGroup的子组流 groupsRepository.groupFlow(groupId).flatMapLatest { group -> groupsRepository.subGroupsFlow(group?.id) }, // 依赖currentGroup的子设备流 groupsRepository.groupFlow(groupId).flatMapLatest { group -> liteDeviceRepository.subDevicesFlow(group?.id) }, // 独立的全设备流 liteDeviceRepository.allDevicesFlow() ) { currentGroup, subGroups, subDevices, allDevices -> Timber.e("Updating UIState with latest values") DevicesUIState( currentGroup = currentGroup, currentGroupSubGroups = subGroups, currentGroupSubDevices = subDevices, allDevices = allDevices ) }.stateIn( scope = viewModelScope, started = SharingStarted.WhileSubscribed(5_000L), initialValue = DevicesUIState() )
优化说明
combine操作符仅在任意一个Flow发射新值时,才会结合所有Flow的最新值生成新UIState,避免了链式嵌套导致的无意义重复触发。- 依赖currentGroup的subGroups、subDevices流,通过
flatMapLatest监听currentGroup变化,动态切换对应数据源,保证数据与当前Group同步。 - allDevices流独立参与合并,它的更新只会触发一次UIState构建,不会牵连其他不相关流程。
进一步优化(可选)
如果groupsRepository.groupFlow(groupId)会被多次订阅,可以先将其缓存为StateFlow,避免重复查询Room数据库:
val groupId: Int? // 缓存currentGroup流,避免重复订阅 private val currentGroupFlow = groupsRepository.groupFlow(groupId) .stateIn( scope = viewModelScope, started = SharingStarted.WhileSubscribed(5_000L), initialValue = null ) val uiState: StateFlow<DevicesUIState> = combine( currentGroupFlow, currentGroupFlow.flatMapLatest { group -> groupsRepository.subGroupsFlow(group?.id) }, currentGroupFlow.flatMapLatest { group -> liteDeviceRepository.subDevicesFlow(group?.id) }, liteDeviceRepository.allDevicesFlow() ) { currentGroup, subGroups, subDevices, allDevices -> DevicesUIState( currentGroup = currentGroup, currentGroupSubGroups = subGroups, currentGroupSubDevices = subDevices, allDevices = allDevices ) }.stateIn( scope = viewModelScope, started = SharingStarted.WhileSubscribed(5_000L), initialValue = DevicesUIState() )
内容的提问来源于stack exchange,提问作者user2408952
相关产品推荐
相关产品推荐

