我有一个 StateFlow,如果三个流之一产生新值,它应该改变:
class FlowViewmodel:ViewModel(){
val currentWeek =MutableStateFlow<String>("start week")
val _overlayRequested:MutableStateFlow<String> = MutableStateFlow("")
val _overlayChanged:MutableStateFlow<ScheduleLoadingState> =
MutableStateFlow(ScheduleLoadingState.Loading)
@OptIn(ExperimentalCoroutinesApi::class)
val weekSchedule:StateFlow<ScheduleLoadingState> = flowOf(
currentWeek.flatMapLatest { week->getWeekSchedule(week) },
_overlayRequested.flatMapLatest { scheduleOwner->
if(scheduleOwner !=""){
requestOverlaySchedule(scheduleOwner)
}else{
flowOf()
}
},
_overlayChanged
)
.flattenMerge()
.stateIn(scope=viewModelScope,SharingStarted.WhileSubscribed(5000), initialValue = ScheduleLoadingState.Loading)
private fun getWeekSchedule(week: String)= flow<ScheduleLoadingState> {
val oldLoadingState:ScheduleLoadingState = weekSchedule.value
emit(ScheduleLoadingState.Loading)
emit(
try {
//if there is an oldPackage update all schedules for requested week
if (oldLoadingState is ScheduleLoadingState.Success) {
val returnedResult =
RequestResult.Success(
SchedulePackage(
"skeleton for $week",
"schedule for $week",
oldLoadingState.schedulePackage.overlaySchedules)
)
if (returnedResult is RequestResult.Success) {
ScheduleLoadingState.Success(returnedResult.data)
} else {
ScheduleLoadingState.Error(
(returnedResult as RequestResult.Failure).error
)
}
} else {
//else get new package with just the schedule of the user
val newPackage = RequestResult.Success(
SchedulePackage("skeleton for $week","schedule for $week")
)
if (newPackage is RequestResult.Success) {
ScheduleLoadingState.Success(newPackage.data)
} else {
ScheduleLoadingState.Error(
(newPackage as RequestResult.Failure).error
)
}
}
}
//as exceptions are caught this can go but keep it to be on the safe side
catch (e: Exception) {
ScheduleLoadingState.Error("error in getWeekSchedule")
}
)
}
private fun requestOverlaySchedule(scheduleOwner: String)=flow<ScheduleLoadingState> {
val weekScheduleState = weekSchedule.value
when (weekScheduleState) {
is ScheduleLoadingState.Success -> {
emit(ScheduleLoadingState.Loading)
weekScheduleState.schedulePackage.overlaySchedules.add(scheduleOwner)
val newPackage = RequestResult.Success(
weekScheduleState.schedulePackage
)
if (newPackage is RequestResult.Success<SchedulePackage>) {
emit(ScheduleLoadingState.Success(newPackage.data))
} else {
emit(ScheduleLoadingState.Error(
(newPackage as RequestResult.Failure).error)
)
}
}
is ScheduleLoadingState.Loading -> {}
is ScheduleLoadingState.Error -> {}
}
}
fun requestOverlay(scheduleOwner: String){
viewModelScope.launch {
_overlayRequested.emit(scheduleOwner)
}
}
fun removeOverlay(overlayId: Int){
val scheduleState = weekSchedule.value
if ( scheduleState is ScheduleLoadingState.Success){
scheduleState.schedulePackage.overlaySchedules.removeAt(overlayId)
_overlayChanged.update{
ScheduleLoadingState.Success(scheduleState.schedulePackage)
}
}
}
}
必修课程
data class SchedulePackage(
val skeleton:String,
val schedule: String,
val overlaySchedules:MutableList<String> = mutableListOf(),
){
var overlayToShow:Int = -1
var message:String = ""
val maximumOverlaysReached:Boolean
get() = overlaySchedules.size == 4
fun addOverlay(overlaySchedule:String){
overlaySchedules.add(overlaySchedule)
}
}
sealed class ScheduleLoadingState {
object Loading:ScheduleLoadingState()
data class Error(val error:String):ScheduleLoadingState()
data class Success(val schedulePackage:SchedulePackage):ScheduleLoadingState()
}
和必要的依赖项
implementation 'androidx.core:core-ktx:1.8.0'
implementation 'androidx.appcompat:appcompat:1.5.0'
implementation 'com.google.android.material:material:1.6.1'
def kotlin_coroutines_version = "1.7.2"
implementation "org.jetbrains.kotlinx:kotlinx-coroutines-core:$kotlin_coroutines_version"
implementation "org.jetbrains.kotlinx:kotlinx-coroutines-android:$kotlin_coroutines_version"
//lifecycle components
def lifecycle_version = "2.4.1"
// ViewModel
implementation "androidx.lifecycle:lifecycle-viewmodel-ktx:$lifecycle_version"
// LiveData
implementation "androidx.lifecycle:lifecycle-livedata-ktx:$lifecycle_version"
// Annotation processor
implementation "androidx.lifecycle:lifecycle-common-java8:$lifecycle_version"
testImplementation 'junit:junit:4.13.2'
androidTestImplementation 'androidx.test.ext:junit:1.1.5'
androidTestImplementation 'androidx.test.espresso:espresso-core:3.5.1'
testImplementation "org.jetbrains.kotlinx:kotlinx-coroutines-test:$kotlin_coroutines_version"
testImplementation "androidx.test.ext:truth:1.5.0"
testImplementation "app.cash.turbine:turbine:1.0.0"
当我测试添加覆盖时,一切正常。 requestOverlaySchedule 生成新的调度,weekSchedule 发出新的状态。
接下来,我在测试中添加了删除覆盖层的功能。现在,当我收集 weekSchedule 流的值时,测试超时,3 秒内没有产生任何值
app.cash.turbine.TurbineAssertionError: No value produced in 3s
我在测试中收集了_overlayChanges,看看它是否确实改变了它的值。我通过删除私有修饰符并将其收集到我的测试中来确认这一点。我为此使用 app.cash.turbine 。测试是这样的:
class FlowViewmodelTest {
@Before
fun setUp() {
Dispatchers.setMain(StandardTestDispatcher())
}
@After
fun tearDown() {
Dispatchers.resetMain()
}
@Test
fun testFlattenMerge(){
runTest{
turbineScope {
val viewModelUnderTest = FlowViewmodel()
val currentWeekFlow = viewModelUnderTest.currentWeek.testIn(backgroundScope)
val overlayRequestedFlow = viewModelUnderTest._overlayRequested.testIn(backgroundScope)
val overlayChangedFlow = viewModelUnderTest._overlayChanged.testIn(backgroundScope)
//the next one is the combined flow
val weekScheduleFlow = viewModelUnderTest.weekSchedule.testIn(backgroundScope)
advanceUntilIdle()
//assert that all base flows are initilized
val firstvalue1 = currentWeekFlow.awaitItem()
val firstvalue2 = overlayRequestedFlow.awaitItem()
val firstvalue3 = overlayChangedFlow.awaitItem()
assertThat(firstvalue1).isEqualTo("start week")
assertThat(firstvalue2).isEqualTo("")
assertThat(firstvalue3).isEqualTo(ScheduleLoadingState.Loading)
//weekschedule cycles through Loading and then succes withj schedule of start week
val firstCombinedValue = weekScheduleFlow.awaitItem()
val secondCombinedValue = weekScheduleFlow.awaitItem()
assertThat(firstCombinedValue).isEqualTo(ScheduleLoadingState.Loading)
assertThat(secondCombinedValue).isInstanceOf(ScheduleLoadingState.Success::class.java)
assertThat((secondCombinedValue as ScheduleLoadingState.Success).schedulePackage.schedule).isEqualTo("schedule for start week")
weekScheduleFlow.expectNoEvents()
viewModelUnderTest.requestOverlay("request overlay for kli")
advanceUntilIdle()
currentWeekFlow.expectNoEvents()
overlayChangedFlow.expectNoEvents()
val secondFlow2Value = overlayRequestedFlow.awaitItem()
assertThat(secondFlow2Value).isEqualTo("request overlay for kli")
val fourthCombinedValue = weekScheduleFlow.awaitItem()
val fourthACombinedValue = weekScheduleFlow.awaitItem()
assertThat(fourthCombinedValue).isEqualTo(ScheduleLoadingState.Loading)
assertThat(fourthACombinedValue).isInstanceOf(ScheduleLoadingState.Success::class.java)
assertThat((fourthACombinedValue as ScheduleLoadingState.Success).schedulePackage.schedule).isEqualTo("schedule for start week")
val schedulePackage = (fourthACombinedValue as ScheduleLoadingState.Success).schedulePackage
assertThat(schedulePackage.overlaySchedules).isNotEmpty()
assertThat(schedulePackage.overlaySchedules.size).isEqualTo(1)
assertThat(schedulePackage.overlaySchedules.first()).isEqualTo("request overlay for kli")
viewModelUnderTest.removeOverlay(0)
advanceUntilIdle()
val secondFlow3Value = overlayChangedFlow.awaitItem()
currentWeekFlow.expectNoEvents()
overlayRequestedFlow.expectNoEvents()
assertThat(secondFlow3Value).isInstanceOf(ScheduleLoadingState.Success::class.java)
assertThat((secondFlow3Value as ScheduleLoadingState.Success).schedulePackage.overlaySchedules).isEmpty()
val sixCombinedValues = weekScheduleFlow.awaitItem() //This one produces no data in 3s
weekScheduleFlow.expectNoEvents()
assertThat(sixCombinedValues).isEqualTo(secondFlow3Value)
}
}
}
}
所以我的问题是,为什么 weekSchedule 对 _overlayChanged 发出新值没有反应?据我了解,当其中一个组合流发出一个值时,flattenMerge 会“发出”一个新值。
我也尝试使用merge(flow1, flow2, flow3),应该有相同的效果。这也行不通。
问题是您期望流程
weekSchedule
再次提供相同(如 equal)的 Success 对象。然而,该流是一个 StateFlow,这意味着具有相同值的连续更新将被忽略。来自文档:
状态流中的值使用 Any.equals 比较进行合并,其方式与distinctUntilChanged 运算符类似。
毕竟,StateFlow 代表单个值,当该值仍然相同时,不应更改任何内容。
现在,您可能认为值did发生了变化:毕竟,您从列表中删除了一个元素。让我们看看调用
viewModelUnderTest.requestOverlay
后流程包含哪些内容:
Success(schedulePackage=SchedulePackage(
skeleton=skeleton for start week,
schedule=schedule for start week,
overlaySchedules=[request overlay for kli],
))
这就是
viewModelUnderTest.removeOverlay
被称为后的样子:
Success(schedulePackage=SchedulePackage(
skeleton=skeleton for start week,
schedule=schedule for start week,
overlaySchedules=[],
))
显然,
overlaySchedules
现在是空的,它之前包含一个值。但你在这里看到的只是一个字符串表示。在这两种情况下,overlaySchedules
实际上仍然是同一个对象:A MutableList<String>
。只有列表的content发生了变化,列表本身没有变化。尽管您使用 创建了一个新的
Success
对象
ScheduleLoadingState.Success(scheduleState.schedulePackage)
其内容仍然是具有相同
SchedulePackage
的相同 MutableList
对象。当 StateFlow 现在将两个值与 equals
进行比较时,它发现它们具有相同的属性,并假设值没有改变。它不会发出新值,并且测试中的以下行永远不会获得新值,直到 3 秒后超时:
val sixCombinedValues = weekScheduleFlow.awaitItem()
关于为什么发生了这么多。现在,可以采取什么措施来解决这个问题?
简单的解决方案是将
MutableList
设为不可变列表:
data class SchedulePackage(
val skeleton: String,
val schedule: String,
val overlaySchedules: List<String> = emptyList(),
)
现在,您显然无法在不可变列表中添加或删除项目,因此您需要在每次更改时创建一个新项目。但这就是重点:我们想要一个新对象,这样它就不会再注册为 equal 了:
val newPackage = RequestResult.Success(
weekScheduleState.schedulePackage.copy(
overlaySchedules = weekScheduleState.schedulePackage.overlaySchedules + scheduleOwner
)
)
_overlayChanged.update {
ScheduleLoadingState.Success(
scheduleState.schedulePackage.copy(
overlaySchedules = scheduleState.schedulePackage.overlaySchedules.let {
it - it[overlayId]
}
)
)
}
这将解决您的问题,测试现在应该通过。
但是,您的流程交织在一起的方式使其逻辑难以理解和维护:您亲身体验了流程变得多么不透明,以及为什么您首先发布了您的问题。您也已经体验到更改某些内容有多么困难:您花了多次尝试,并且可能花费了大量时间来创建示例。
到目前为止,从我对你的代码的了解来看,它可能可以大大简化。关键是要有一个清晰的层次结构,明确哪个流依赖哪个流,并在需要另一个流的值来创建新值时使用
combine
。请记住:始终使用不可变对象作为状态。
我强烈建议您尽快清理它。如果无人看管,它肯定会在一段时间后回来咬你。
如果您希望有人验证您的重构,请随时使用姊妹网站 https://codereview.stackexchange.com/ 以获得第二意见。您可以使用与此处相同的用户在那里发帖。该网站的工作原理与 Stack Overflow 相同,只是它们只关注已经按预期工作的代码。他们不提供错误修复,而是提供代码审查,并提供有关如何改进代码的指导。