Kotlin flow.Flattenmerge 似乎不起作用

问题描述 投票:0回答:1

我有一个 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),应该有相同的效果。这也行不通。

kotlin kotlin-flow
1个回答
0
投票

问题是您期望流程

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 相同,只是它们只关注已经按预期工作的代码。他们不提供错误修复,而是提供代码审查,并提供有关如何改进代码的指导。

© www.soinside.com 2019 - 2024. All rights reserved.