如何避免StateFlow联动时DaySchedule的无效更新?
问题与解决方案
问题场景
现有一组关联的StateFlow用于处理日程数据:
val _currentDay = MutableStateFlow<Day>(Day()) val currentDay: StateFlow<Day> = _currentDay val currentWeek = currentDay .map { day -> getWeekFrom(day) } .stateIn( viewModelScope, SharingStarted.WhileSubscribed(5000), initialValue = Week(currentDay.value), ) val weekSchedule: StateFlow<ScheduleLoadingState> = currentWeek .flatMapLatest { week -> getWeekSchedule(week) } .stateIn( scope = viewModelScope, SharingStarted.WhileSubscribed(5000), initialValue = ScheduleLoadingState.Loading, ) @OptIn(FlowPreview::class) val daySchedule: StateFlow<ScheduleLoadingState> = weekSchedule .combine(currentDay) { weekschedule, day -> getDayScheduleFromWeekSchedule(day, weekschedule) } .stateIn( scope = viewModelScope, SharingStarted.WhileSubscribed(5000), initialValue = ScheduleLoadingState.Loading, )
当日期切换到新周时,daySchedule会先基于旧的WeekSchedule生成一次无效的错误日程,之后才会更新为新WeekSchedule对应的正确结果。需要实现仅在以下两种场景触发daySchedule更新:
WeekSchedule本身完成更新WeekSchedule未变化,但currentDay在同周内更新
解决方案
方法1:通过Flow操作符联动过滤无效更新
利用flatMapLatest的层级联动,确保只有当前日期属于currentWeek且对应weekSchedule已加载时,才生成日程:
val daySchedule: StateFlow<ScheduleLoadingState> = currentWeek .flatMapLatest { currentWeek -> // 当周变化时,先等待新的weekSchedule加载完成 weekSchedule .flatMapLatest { scheduleState -> // 仅处理属于当前周的日期变化 currentDay .filter { getWeekFrom(it) == currentWeek } .map { day -> getDayScheduleFromWeekSchedule(day, scheduleState) } } } .stateIn( scope = viewModelScope, SharingStarted.WhileSubscribed(5000), initialValue = ScheduleLoadingState.Loading, )
逻辑说明:
- 当
currentWeek更新时,外层flatMapLatest会自动取消旧的流,切换到新周的处理逻辑 - 内层等待新的
weekSchedule加载完成后,再监听currentDay的变化,且只过滤出属于当前周的日期,从根源避免旧周数据处理新周日期的问题
方法2:在Combine时加入周匹配判断
如果需要保留combine的结构,可以加入currentWeek一起联动,判断当前日期是否属于weekSchedule对应的周:
@OptIn(FlowPreview::class) val daySchedule: StateFlow<ScheduleLoadingState> = combine( weekSchedule, currentDay, currentWeek ) { scheduleState, day, currentWeek -> when (scheduleState) { is ScheduleLoadingState.Loading -> ScheduleLoadingState.Loading is ScheduleLoadingState.Loaded -> { // 仅当日期属于当前weekSchedule对应的周时,生成有效日程 if (getWeekFrom(day) == scheduleState.week) { getDayScheduleFromWeekSchedule(day, scheduleState) } else { // 日期不属于当前schedule的周,返回Loading或保留上一次有效值 ScheduleLoadingState.Loading } } is ScheduleLoadingState.Error -> scheduleState } } .stateIn( scope = viewModelScope, SharingStarted.WhileSubscribed(5000), initialValue = ScheduleLoadingState.Loading, )
补充优化:如果不想在无效状态下返回Loading,可以用scan操作符维护上一个有效日程状态,避免界面闪烁:
@OptIn(FlowPreview::class) val daySchedule: StateFlow<ScheduleLoadingState> = combine( weekSchedule, currentDay, currentWeek ) { scheduleState, day, currentWeek -> Pair(scheduleState, day) } .scan(ScheduleLoadingState.Loading) { lastValidState, (scheduleState, day) -> when (scheduleState) { is ScheduleLoadingState.Loaded -> { if (getWeekFrom(day) == scheduleState.week) { getDayScheduleFromWeekSchedule(day, scheduleState) } else { lastValidState // 保留上一次有效状态 } } else -> scheduleState } } .stateIn( scope = viewModelScope, SharingStarted.WhileSubscribed(5000), initialValue = ScheduleLoadingState.Loading, )
方法3:追踪Flow更新源
通过给Flow发射值添加标记,明确区分是weekSchedule更新还是currentDay更新,再决定是否触发计算:
@OptIn(FlowPreview::class) val daySchedule: StateFlow<ScheduleLoadingState> = merge( weekSchedule.map { Pair(it, "week") }, currentDay.map { Pair(it, "day") } ) .scan( initialValue = Triple(weekSchedule.value, currentDay.value, currentWeek.value) ) { (oldSchedule, oldDay, oldWeek), newPair -> when (newPair.second) { "week" -> Triple(newPair.first as ScheduleLoadingState, oldDay, getWeekFrom(oldDay)) "day" -> { val newDay = newPair.first as Day val newWeek = getWeekFrom(newDay) Triple(oldSchedule, newDay, newWeek) } else -> Triple(oldSchedule, oldDay, oldWeek) } } .filter { (schedule, day, week) -> // 仅当schedule对应的周与当前day的周匹配,或者是schedule更新时 (schedule is ScheduleLoadingState.Loaded && schedule.week == week) || (schedule is ScheduleLoadingState.Loading || schedule is ScheduleLoadingState.Error) } .map { (schedule, day, _) -> getDayScheduleFromWeekSchedule(day, schedule) } .stateIn( scope = viewModelScope, SharingStarted.WhileSubscribed(5000), initialValue = ScheduleLoadingState.Loading, )
逻辑说明:
- 用
merge将两个Flow包装成带来源标记的配对值 - 用
scan维护当前的schedule、day、week状态 - 通过
filter过滤掉日期与schedule周不匹配的无效更新
内容的提问来源于stack exchange,提问作者KvdLingen
相关产品推荐
相关产品推荐

