使用RoomDao与Kotlin协程、Flow时,数据变更后Flow未触发的问题排查
看起来你遇到的核心问题是:当更新Exercise数据时,trainingExerciseLinksRepository.getTrainingExerciseLinksBy(trainingId)返回的Flow没有触发更新。这是因为Room的Flow触发机制完全基于查询涉及的表的变化——你的getTrainingExerciseLinksBy查询只针对TrainingExerciseLink表,而Exercise数据的变更不会影响这个关联表的内容,所以Room不会发射新的查询结果,下游的Flow链自然也不会更新。
下面给出两种针对性的解决方案:
方案一:修改DAO查询,关联Exercise表以监听其变化
你可以调整TrainingExerciseLinkDao的查询语句,通过LEFT JOIN关联Exercise表。这样一来,当Exercise表中的数据发生变更时,Room会检测到关联表的变化,从而触发getTrainingExerciseLinksBy的Flow发射新值。
修改后的DAO代码:
@Dao abstract class TrainingExerciseLinkDao { @Query(""" SELECT tel.* FROM TrainingExerciseLink tel LEFT JOIN Exercise e ON tel.exerciseId = e.id WHERE tel.trainingId = :trainingId """) abstract fun getTrainingExerciseLinksBy(trainingId: Long): Flow<List<TrainingExerciseLink>> }
这个查询仍然返回TrainingExerciseLink列表,但因为关联了Exercise表,Room会同时监控Exercise表的变更,一旦Exercise数据更新,就会重新执行查询并发射新的结果,触发后续的Flow处理逻辑。
方案二:在UseCase中主动监听Exercise的变化
如果你不想修改DAO的查询,可以在UseCase中为每个TrainingExerciseLink同时监听对应的Exercise Flow和Repetition Flow,通过combine操作符将它们组合起来。这样当Exercise或Repetition数据更新时,都会触发对应的子Flow,进而更新整个数据流。
首先假设你有一个获取单个Exercise的Repository方法:
// ExercisesRepository中的方法 fun getExerciseById(exerciseId: Long): Flow<Exercise>
然后修改你的UseCase代码:
suspend fun getTrainingWithExercisesAndRepetitionsBy(trainingId: Long): Flow<UiTrainingWithExercisesAndRepetitions> { return trainingExerciseLinksRepository.getTrainingExerciseLinksBy(trainingId).flatMapConcat { trainingExerciseLinks -> // 为每个关联项组合Exercise和Repetitions的Flow val exerciseFlows = trainingExerciseLinks.map { link -> combine( exercisesRepository.getExerciseById(link.exerciseId), repetitionsRepository.getExerciseRepetitionsBy(link.id) ) { exercise, repetitions -> // 组装单个Exercise及其Repetitions的UI模型 UiExerciseWithRepetitions(exercise, repetitions) }.flowOn(Dispatchers.IO) } // 使用combineAll替代zipFlows,确保任何子Flow更新都会触发整体更新 exerciseFlows.combineAll() }.map { exerciseList -> // 这里需要获取Training数据,如果Training也可能变更,建议也用Flow监听 val training = trainingRepository.getTrainingById(trainingId) // 若为Flow,需调整为combine UiTrainingWithExercisesAndRepetitions(training, exerciseList) }.flowOn(Dispatchers.IO) } // 自定义combineAll操作符,将多个Flow组合成一个Flow<List<T>> fun <T> Iterable<Flow<T>>.combineAll(): Flow<List<T>> { return combine(this) { it.toList() } }
这里的关键是用combine替代单纯依赖TrainingExerciseLink的Flow,主动监听Exercise的变化;同时用combineAll替代zipFlows(如果你的zipFlows是基于zip实现的),因为zip需要所有子Flow都发射新值才会更新,而combine只要有一个子Flow更新就会触发整体更新,更符合你的需求。
额外注意事项
- 确保你的UI模型(如
UiTrainingWithExercisesAndRepetitions、UiExerciseWithRepetitions)正确实现了equals()和hashCode()方法,这样distinctUntilChanged()才能正常工作,避免不必要的UI更新。 - 如果Training数据也可能发生变更,建议同样将其作为Flow加入到组合逻辑中,确保Training更新时UI也能同步刷新。
内容的提问来源于stack exchange,提问作者Sergey Grishin

