diff --git a/mvvm-state/build.gradle.kts b/mvvm-state-core/build.gradle.kts similarity index 87% rename from mvvm-state/build.gradle.kts rename to mvvm-state-core/build.gradle.kts index ee57500e..5be8a4a5 100644 --- a/mvvm-state/build.gradle.kts +++ b/mvvm-state-core/build.gradle.kts @@ -9,8 +9,6 @@ plugins { } dependencies { - commonMainApi(projects.mvvmLivedata) - commonTestApi(libs.mokoTest) commonTestApi(projects.mvvmTest) } diff --git a/mvvm-state/src/androidMain/AndroidManifest.xml b/mvvm-state-core/src/androidMain/AndroidManifest.xml similarity index 100% rename from mvvm-state/src/androidMain/AndroidManifest.xml rename to mvvm-state-core/src/androidMain/AndroidManifest.xml diff --git a/mvvm-state-core/src/commonMain/kotlin/dev/icerock/moko/mvvm/state/Merges.kt b/mvvm-state-core/src/commonMain/kotlin/dev/icerock/moko/mvvm/state/Merges.kt new file mode 100644 index 00000000..bd2c7ff9 --- /dev/null +++ b/mvvm-state-core/src/commonMain/kotlin/dev/icerock/moko/mvvm/state/Merges.kt @@ -0,0 +1,23 @@ +/* + * Copyright 2022 IceRock MAG Inc. Use of this source code is governed by the Apache 2.0 license. + */ + +package dev.icerock.moko.mvvm.state + +fun mergeState( + firstState: ResourceState, + secondState: ResourceState, + function: (T1, T2) -> OT +): ResourceState = when { + (firstState is ResourceState.Loading || secondState is ResourceState.Loading) -> ResourceState.Loading() + (firstState is ResourceState.Failed) -> ResourceState.Failed(firstState.error) + (secondState is ResourceState.Failed) -> ResourceState.Failed(secondState.error) + (firstState is ResourceState.Empty || secondState is ResourceState.Empty) -> ResourceState.Empty() + (firstState is ResourceState.Success && secondState is ResourceState.Success) -> ResourceState.Success( + function( + firstState.data, + secondState.data + ) + ) + else -> ResourceState.Empty() +} diff --git a/mvvm-state-core/src/commonMain/kotlin/dev/icerock/moko/mvvm/state/ResourceState.kt b/mvvm-state-core/src/commonMain/kotlin/dev/icerock/moko/mvvm/state/ResourceState.kt new file mode 100644 index 00000000..4cee0845 --- /dev/null +++ b/mvvm-state-core/src/commonMain/kotlin/dev/icerock/moko/mvvm/state/ResourceState.kt @@ -0,0 +1,21 @@ +/* + * Copyright 2022 IceRock MAG Inc. Use of this source code is governed by the Apache 2.0 license. + */ + +package dev.icerock.moko.mvvm.state + +sealed class ResourceState { + data class Success(val data: TData) : ResourceState() + data class Failed(val error: TError) : ResourceState() + class Loading : ResourceState() + class Empty : ResourceState() + + fun isLoading(): Boolean = this is Loading + fun isSuccess(): Boolean = this is Success + fun isEmpty(): Boolean = this is Empty + fun isFailed(): Boolean = this is Failed + + fun dataValue(): TData? = (this as? Success)?.data + + fun errorValue(): TError? = (this as? Failed)?.error +} diff --git a/mvvm-state/src/commonMain/kotlin/dev/icerock/moko/mvvm/StateExt.kt b/mvvm-state-core/src/commonMain/kotlin/dev/icerock/moko/mvvm/state/StateExt.kt similarity index 88% rename from mvvm-state/src/commonMain/kotlin/dev/icerock/moko/mvvm/StateExt.kt rename to mvvm-state-core/src/commonMain/kotlin/dev/icerock/moko/mvvm/state/StateExt.kt index 1aecbeca..9c6a79d4 100644 --- a/mvvm-state/src/commonMain/kotlin/dev/icerock/moko/mvvm/StateExt.kt +++ b/mvvm-state-core/src/commonMain/kotlin/dev/icerock/moko/mvvm/state/StateExt.kt @@ -1,8 +1,8 @@ /* - * Copyright 2019 IceRock MAG Inc. Use of this source code is governed by the Apache 2.0 license. + * Copyright 2022 IceRock MAG Inc. Use of this source code is governed by the Apache 2.0 license. */ -package dev.icerock.moko.mvvm +package dev.icerock.moko.mvvm.state fun T.asState(): ResourceState = ResourceState.Success(this) diff --git a/mvvm-state-flow/build.gradle.kts b/mvvm-state-flow/build.gradle.kts new file mode 100644 index 00000000..4f0e9bf7 --- /dev/null +++ b/mvvm-state-flow/build.gradle.kts @@ -0,0 +1,17 @@ +/* + * Copyright 2019 IceRock MAG Inc. Use of this source code is governed by the Apache 2.0 license. + */ + +plugins { + id("kmp-library-convention") + id("detekt-convention") + id("publication-convention") +} + +dependencies { + commonMainApi(libs.coroutines) + commonMainApi(projects.mvvmStateCore) + + commonTestApi(libs.mokoTest) + commonTestApi(projects.mvvmStateTest) +} diff --git a/mvvm-state-flow/src/androidMain/AndroidManifest.xml b/mvvm-state-flow/src/androidMain/AndroidManifest.xml new file mode 100755 index 00000000..9aa5adca --- /dev/null +++ b/mvvm-state-flow/src/androidMain/AndroidManifest.xml @@ -0,0 +1,2 @@ + + \ No newline at end of file diff --git a/mvvm-state-flow/src/commonMain/kotlin/dev/icerock/moko/mvvm/flow/FlowStateConditions.kt b/mvvm-state-flow/src/commonMain/kotlin/dev/icerock/moko/mvvm/flow/FlowStateConditions.kt new file mode 100644 index 00000000..0802437a --- /dev/null +++ b/mvvm-state-flow/src/commonMain/kotlin/dev/icerock/moko/mvvm/flow/FlowStateConditions.kt @@ -0,0 +1,44 @@ +/* + * Copyright 2022 IceRock MAG Inc. Use of this source code is governed by the Apache 2.0 license. + */ + +@file:Suppress("MaximumLineLength", "MaxLineLength") + +package dev.icerock.moko.mvvm.flow + +import dev.icerock.moko.mvvm.state.ResourceState +import kotlinx.coroutines.flow.Flow +import kotlinx.coroutines.flow.combine +import kotlinx.coroutines.flow.map + +fun Flow>.isSuccessState(): Flow = + map { it.isSuccess() } + +fun Flow>.isLoadingState(): Flow = + map { it.isLoading() } + +fun Flow>.isErrorState(): Flow = + map { it.isFailed() } + +fun Flow>.isEmptyState(): Flow = + map { it.isEmpty() } + +inline fun , FL : Flow> List.isSuccessState(): Flow = + combine(this) { list -> + list.firstOrNull { it !is ResourceState.Success<*, *> } == null + } + +inline fun , F : Flow> List.isLoadingState(): Flow = + combine(this) { list -> + list.firstOrNull() { it is ResourceState.Loading<*, *> } != null + } + +inline fun , F : Flow> List.isErrorState(): Flow = + combine(this) { list -> + list.firstOrNull { it is ResourceState.Failed<*, *> } != null + } + +inline fun , F : Flow> List.isEmptyState(): Flow = + combine(this) { list -> + list.firstOrNull { it is ResourceState.Empty<*, *> } != null + } diff --git a/mvvm-state-flow/src/commonMain/kotlin/dev/icerock/moko/mvvm/flow/FlowStateExt.kt b/mvvm-state-flow/src/commonMain/kotlin/dev/icerock/moko/mvvm/flow/FlowStateExt.kt new file mode 100644 index 00000000..505b08e3 --- /dev/null +++ b/mvvm-state-flow/src/commonMain/kotlin/dev/icerock/moko/mvvm/flow/FlowStateExt.kt @@ -0,0 +1,39 @@ +/* + * Copyright 2022 IceRock MAG Inc. Use of this source code is governed by the Apache 2.0 license. + */ + +package dev.icerock.moko.mvvm.flow + +import dev.icerock.moko.mvvm.state.ResourceState +import kotlinx.coroutines.flow.Flow +import kotlinx.coroutines.flow.StateFlow +import kotlinx.coroutines.flow.combine +import kotlinx.coroutines.flow.lastOrNull +import kotlinx.coroutines.flow.map + +fun Flow>.data(): Flow = + map { it.dataValue() } + +fun StateFlow>.dataValue(): TData? = + value.dataValue() + +suspend fun Flow>.dataValue(): TData? = + lastOrNull()?.dataValue() + +fun Flow>.error(): Flow = + map { it.errorValue() } + +fun StateFlow>.errorValue(): TError? = + value.errorValue() + +suspend fun Flow>.errorValue(): TError? = + lastOrNull()?.errorValue() + +inline fun , LD : Flow> List.error(): Flow = + combine(this) { list -> + @Suppress("UNCHECKED_CAST") + val errorItem = list.firstOrNull { + (it is ResourceState.Failed<*, *>) + } as? ResourceState.Failed + errorItem?.error + } diff --git a/mvvm-state-flow/src/commonMain/kotlin/dev/icerock/moko/mvvm/flow/FlowStateMerges.kt b/mvvm-state-flow/src/commonMain/kotlin/dev/icerock/moko/mvvm/flow/FlowStateMerges.kt new file mode 100644 index 00000000..7244140c --- /dev/null +++ b/mvvm-state-flow/src/commonMain/kotlin/dev/icerock/moko/mvvm/flow/FlowStateMerges.kt @@ -0,0 +1,20 @@ +/* + * Copyright 2022 IceRock MAG Inc. Use of this source code is governed by the Apache 2.0 license. + */ + +package dev.icerock.moko.mvvm.flow + +import dev.icerock.moko.mvvm.state.ResourceState +import dev.icerock.moko.mvvm.state.mergeState +import kotlinx.coroutines.flow.Flow +import kotlinx.coroutines.flow.combine + +fun Flow>.concatData( + flow: Flow>, + function: (T1, T2) -> OT +): Flow> = + combine( + this, flow + ) { firstState, secondState -> + mergeState(firstState, secondState, function) + } diff --git a/mvvm-state-flow/src/commonMain/kotlin/dev/icerock/moko/mvvm/flow/FlowStateTransforms.kt b/mvvm-state-flow/src/commonMain/kotlin/dev/icerock/moko/mvvm/flow/FlowStateTransforms.kt new file mode 100644 index 00000000..dea11276 --- /dev/null +++ b/mvvm-state-flow/src/commonMain/kotlin/dev/icerock/moko/mvvm/flow/FlowStateTransforms.kt @@ -0,0 +1,65 @@ +/* + * Copyright 2022 IceRock MAG Inc. Use of this source code is governed by the Apache 2.0 license. + */ + +@file:OptIn(ExperimentalCoroutinesApi::class) + +package dev.icerock.moko.mvvm.flow + +import dev.icerock.moko.mvvm.state.ResourceState +import kotlinx.coroutines.ExperimentalCoroutinesApi +import kotlinx.coroutines.flow.Flow +import kotlinx.coroutines.flow.flatMapLatest +import kotlinx.coroutines.flow.flowOf +import kotlinx.coroutines.flow.map + +fun Flow>.dataTransform( + transform: Flow.() -> Flow +): Flow> = flatMapLatest { state -> + when (state) { + is ResourceState.Success -> transform(flowOf(state.data)) + .map { ResourceState.Success(it) } + is ResourceState.Empty -> flowOf(ResourceState.Empty()) + is ResourceState.Failed -> flowOf(ResourceState.Failed(state.error)) + is ResourceState.Loading -> flowOf(ResourceState.Loading()) + } +} + +fun Flow>.errorTransform( + transform: Flow.() -> Flow +): Flow> = flatMapLatest { state -> + when (state) { + is ResourceState.Success -> flowOf(ResourceState.Success(state.data)) + is ResourceState.Loading -> flowOf(ResourceState.Loading()) + is ResourceState.Empty -> flowOf(ResourceState.Empty()) + is ResourceState.Failed -> transform.invoke(flowOf(state.error)) + .map { ResourceState.Failed(it) } + } +} + +fun Flow>.emptyAsError( + errorBuilder: () -> E +): Flow> = map { + when (it) { + is ResourceState.Empty -> ResourceState.Failed(errorBuilder()) + else -> it + } +} + +fun Flow>.emptyAsData( + dataBuilder: () -> T +): Flow> = map { + when (it) { + is ResourceState.Empty -> ResourceState.Success(dataBuilder()) + else -> it + } +} + +fun Flow>.emptyIf( + emptyPredicate: (T) -> Boolean +): Flow> = map { + when { + it is ResourceState.Success && emptyPredicate(it.data) -> ResourceState.Empty() + else -> it + } +} diff --git a/mvvm-state-flow/src/commonTest/kotlin/dev/icerock/moko/mvvm/flow/FlowStateTest.kt b/mvvm-state-flow/src/commonTest/kotlin/dev/icerock/moko/mvvm/flow/FlowStateTest.kt new file mode 100644 index 00000000..a4adefb6 --- /dev/null +++ b/mvvm-state-flow/src/commonTest/kotlin/dev/icerock/moko/mvvm/flow/FlowStateTest.kt @@ -0,0 +1,24 @@ +/* + * Copyright 2020 IceRock MAG Inc. Use of this source code is governed by the Apache 2.0 license. + */ + +package dev.icerock.moko.mvvm.flow + + +import dev.icerock.moko.mvvm.test.flow.FlowTest +import kotlinx.coroutines.ExperimentalCoroutinesApi +import kotlinx.coroutines.test.TestResult +import kotlin.test.Test + +@OptIn(ExperimentalCoroutinesApi::class) +class Test : FlowTest() { + @Test + override fun dataTransformTest(): TestResult { + return super.dataTransformTest() + } + + @Test + override fun dataTransformMergeWithTest(): TestResult { + return super.dataTransformMergeWithTest() + } +} \ No newline at end of file diff --git a/mvvm-state-livedata/build.gradle.kts b/mvvm-state-livedata/build.gradle.kts new file mode 100644 index 00000000..8b508aef --- /dev/null +++ b/mvvm-state-livedata/build.gradle.kts @@ -0,0 +1,17 @@ +/* + * Copyright 2019 IceRock MAG Inc. Use of this source code is governed by the Apache 2.0 license. + */ + +plugins { + id("kmp-library-convention") + id("detekt-convention") + id("publication-convention") +} + +dependencies { + commonMainApi(projects.mvvmLivedata) + commonMainApi(projects.mvvmStateCore) + + commonTestApi(projects.mvvmTest) + commonTestApi(projects.mvvmStateTest) +} diff --git a/mvvm-state-livedata/src/androidMain/AndroidManifest.xml b/mvvm-state-livedata/src/androidMain/AndroidManifest.xml new file mode 100755 index 00000000..c7499644 --- /dev/null +++ b/mvvm-state-livedata/src/androidMain/AndroidManifest.xml @@ -0,0 +1,2 @@ + + \ No newline at end of file diff --git a/mvvm-state-livedata/src/commonMain/kotlin/dev/icerock/moko/mvvm/ResourceState.kt b/mvvm-state-livedata/src/commonMain/kotlin/dev/icerock/moko/mvvm/ResourceState.kt new file mode 100644 index 00000000..3d1caf83 --- /dev/null +++ b/mvvm-state-livedata/src/commonMain/kotlin/dev/icerock/moko/mvvm/ResourceState.kt @@ -0,0 +1,14 @@ +/* + * Copyright 2022 IceRock MAG Inc. Use of this source code is governed by the Apache 2.0 license. + */ + +package dev.icerock.moko.mvvm + +import dev.icerock.moko.mvvm.state.ResourceState + +@Deprecated( + message = "deprecated due to package renaming", + replaceWith = ReplaceWith("ResourceState", "dev.icerock.moko.mvvm.state"), + level = DeprecationLevel.WARNING +) +typealias ResourceState = ResourceState diff --git a/mvvm-state-livedata/src/commonMain/kotlin/dev/icerock/moko/mvvm/StateExt.kt b/mvvm-state-livedata/src/commonMain/kotlin/dev/icerock/moko/mvvm/StateExt.kt new file mode 100644 index 00000000..57632980 --- /dev/null +++ b/mvvm-state-livedata/src/commonMain/kotlin/dev/icerock/moko/mvvm/StateExt.kt @@ -0,0 +1,58 @@ +/* + * Copyright 2019 IceRock MAG Inc. Use of this source code is governed by the Apache 2.0 license. + */ + +package dev.icerock.moko.mvvm + +import dev.icerock.moko.mvvm.state.ResourceState +import dev.icerock.moko.mvvm.state.asState +import dev.icerock.moko.mvvm.state.nullAsEmpty +import dev.icerock.moko.mvvm.state.nullAsLoading + +@Deprecated( + message = "deprecated due to package renaming", + replaceWith = ReplaceWith("asState", "dev.icerock.moko.mvvm.state"), + level = DeprecationLevel.WARNING +) +fun T.asState(): ResourceState = + this.asState() + +@Deprecated( + message = "deprecated due to package renaming", + replaceWith = ReplaceWith("asState", "dev.icerock.moko.mvvm.state"), + level = DeprecationLevel.WARNING +) +fun T?.asState(whenNull: () -> ResourceState): ResourceState = + this?.asState() ?: whenNull() + +@Deprecated( + message = "deprecated due to package renaming", + replaceWith = ReplaceWith("asState", "dev.icerock.moko.mvvm.state"), + level = DeprecationLevel.WARNING +) +fun List.asState(): ResourceState, E> = + this.asState() + +@Deprecated( + message = "deprecated due to package renaming", + replaceWith = ReplaceWith("asState", "dev.icerock.moko.mvvm.state"), + level = DeprecationLevel.WARNING +) +fun List?.asState(whenNull: () -> ResourceState, E>): ResourceState, E> = + this.asState(whenNull) + +@Deprecated( + message = "deprecated due to package renaming", + replaceWith = ReplaceWith("nullAsEmpty", "dev.icerock.moko.mvvm.state"), + level = DeprecationLevel.WARNING +) +inline fun ResourceState?.nullAsEmpty(): ResourceState = + this.nullAsEmpty() + +@Deprecated( + message = "deprecated due to package renaming", + replaceWith = ReplaceWith("nullAsLoading", "dev.icerock.moko.mvvm.state"), + level = DeprecationLevel.WARNING +) +inline fun ResourceState?.nullAsLoading(): ResourceState = + this.nullAsLoading() diff --git a/mvvm-state/src/commonMain/kotlin/dev/icerock/moko/mvvm/livedata/StateLiveDataConditions.kt b/mvvm-state-livedata/src/commonMain/kotlin/dev/icerock/moko/mvvm/livedata/StateLiveDataConditions.kt similarity index 87% rename from mvvm-state/src/commonMain/kotlin/dev/icerock/moko/mvvm/livedata/StateLiveDataConditions.kt rename to mvvm-state-livedata/src/commonMain/kotlin/dev/icerock/moko/mvvm/livedata/StateLiveDataConditions.kt index 664d31e6..2d45021c 100644 --- a/mvvm-state/src/commonMain/kotlin/dev/icerock/moko/mvvm/livedata/StateLiveDataConditions.kt +++ b/mvvm-state-livedata/src/commonMain/kotlin/dev/icerock/moko/mvvm/livedata/StateLiveDataConditions.kt @@ -4,15 +4,19 @@ package dev.icerock.moko.mvvm.livedata -import dev.icerock.moko.mvvm.ResourceState +import dev.icerock.moko.mvvm.state.ResourceState -fun LiveData>.isSuccessState(): LiveData = map { it.isSuccess() } +fun LiveData>.isSuccessState(): LiveData = + map { it.isSuccess() } -fun LiveData>.isLoadingState(): LiveData = map { it.isLoading() } +fun LiveData>.isLoadingState(): LiveData = + map { it.isLoading() } -fun LiveData>.isErrorState(): LiveData = map { it.isFailed() } +fun LiveData>.isErrorState(): LiveData = + map { it.isFailed() } -fun LiveData>.isEmptyState(): LiveData = map { it.isEmpty() } +fun LiveData>.isEmptyState(): LiveData = + map { it.isEmpty() } fun , LD : LiveData> List.isSuccessState(): LiveData = MediatorLiveData(false) diff --git a/mvvm-state/src/commonMain/kotlin/dev/icerock/moko/mvvm/livedata/StateLiveDataExt.kt b/mvvm-state-livedata/src/commonMain/kotlin/dev/icerock/moko/mvvm/livedata/StateLiveDataExt.kt similarity index 64% rename from mvvm-state/src/commonMain/kotlin/dev/icerock/moko/mvvm/livedata/StateLiveDataExt.kt rename to mvvm-state-livedata/src/commonMain/kotlin/dev/icerock/moko/mvvm/livedata/StateLiveDataExt.kt index 5fc6162e..a1dc425c 100644 --- a/mvvm-state/src/commonMain/kotlin/dev/icerock/moko/mvvm/livedata/StateLiveDataExt.kt +++ b/mvvm-state-livedata/src/commonMain/kotlin/dev/icerock/moko/mvvm/livedata/StateLiveDataExt.kt @@ -4,15 +4,19 @@ package dev.icerock.moko.mvvm.livedata -import dev.icerock.moko.mvvm.ResourceState +import dev.icerock.moko.mvvm.state.ResourceState -fun LiveData>.data(): LiveData = map { it.dataValue() } +fun LiveData>.data(): LiveData = + map { it.dataValue() } -fun LiveData>.dataValue(): T? = value.dataValue() +fun LiveData>.dataValue(): T? = + value.dataValue() -fun LiveData>.error(): LiveData = map { it.errorValue() } +fun LiveData>.error(): LiveData = + map { it.errorValue() } -fun LiveData>.errorValue(): E? = value.errorValue() +fun LiveData>.errorValue(): E? = + value.errorValue() fun , LD : LiveData> List.error(): LiveData = MediatorLiveData(null) diff --git a/mvvm-state-livedata/src/commonMain/kotlin/dev/icerock/moko/mvvm/livedata/StateLiveDataMerges.kt b/mvvm-state-livedata/src/commonMain/kotlin/dev/icerock/moko/mvvm/livedata/StateLiveDataMerges.kt new file mode 100644 index 00000000..a7237b87 --- /dev/null +++ b/mvvm-state-livedata/src/commonMain/kotlin/dev/icerock/moko/mvvm/livedata/StateLiveDataMerges.kt @@ -0,0 +1,16 @@ +/* + * Copyright 2021 IceRock MAG Inc. Use of this source code is governed by the Apache 2.0 license. + */ + +package dev.icerock.moko.mvvm.livedata + +import dev.icerock.moko.mvvm.state.ResourceState +import dev.icerock.moko.mvvm.state.mergeState + +fun LiveData>.concatData( + liveData: LiveData>, + function: (T1, T2) -> OT +): LiveData> = + mediatorOf(this, liveData) { firstState, secondState -> + mergeState(firstState, secondState, function) + } diff --git a/mvvm-state/src/commonMain/kotlin/dev/icerock/moko/mvvm/livedata/StateLiveDataTransforms.kt b/mvvm-state-livedata/src/commonMain/kotlin/dev/icerock/moko/mvvm/livedata/StateLiveDataTransforms.kt similarity index 95% rename from mvvm-state/src/commonMain/kotlin/dev/icerock/moko/mvvm/livedata/StateLiveDataTransforms.kt rename to mvvm-state-livedata/src/commonMain/kotlin/dev/icerock/moko/mvvm/livedata/StateLiveDataTransforms.kt index 2f577565..41e32e2b 100644 --- a/mvvm-state/src/commonMain/kotlin/dev/icerock/moko/mvvm/livedata/StateLiveDataTransforms.kt +++ b/mvvm-state-livedata/src/commonMain/kotlin/dev/icerock/moko/mvvm/livedata/StateLiveDataTransforms.kt @@ -4,7 +4,7 @@ package dev.icerock.moko.mvvm.livedata -import dev.icerock.moko.mvvm.ResourceState +import dev.icerock.moko.mvvm.state.ResourceState fun LiveData>.dataTransform( transform: LiveData.() -> LiveData @@ -26,7 +26,7 @@ fun LiveData>.errorTransform( is ResourceState.Loading -> MutableLiveData(ResourceState.Loading()) is ResourceState.Empty -> MutableLiveData(ResourceState.Empty()) is ResourceState.Failed -> transform.invoke(MutableLiveData(state.error)) - .map { ResourceState.Failed(it) } + .map { ResourceState.Failed(it) } } } diff --git a/mvvm-state-livedata/src/commonTest/kotlin/dev/icerock/moko/mvvm/test/livedata/LiveDataStateTest.kt b/mvvm-state-livedata/src/commonTest/kotlin/dev/icerock/moko/mvvm/test/livedata/LiveDataStateTest.kt new file mode 100644 index 00000000..aec4df27 --- /dev/null +++ b/mvvm-state-livedata/src/commonTest/kotlin/dev/icerock/moko/mvvm/test/livedata/LiveDataStateTest.kt @@ -0,0 +1,18 @@ +/* + * Copyright 2022 IceRock MAG Inc. Use of this source code is governed by the Apache 2.0 license. + */ + +package dev.icerock.moko.mvvm.test.livedata + +import kotlin.test.Test + +class Test: LiveDataTest() { + + @Test + override fun dataTransformTest() = + super.dataTransformTest() + + @Test + override fun dataTransformMergeWithTest() = + super.dataTransformMergeWithTest() +} diff --git a/mvvm-state-test/build.gradle.kts b/mvvm-state-test/build.gradle.kts new file mode 100644 index 00000000..c41ba7f0 --- /dev/null +++ b/mvvm-state-test/build.gradle.kts @@ -0,0 +1,17 @@ +/* + * Copyright 2019 IceRock MAG Inc. Use of this source code is governed by the Apache 2.0 license. + */ + +plugins { + id("kmp-library-convention") + id("detekt-convention") +} + +dependencies { + commonMainImplementation(projects.mvvmInternal) + + commonMainApi(projects.mvvmStateFlow) + commonMainApi(projects.mvvmStateLivedata) + commonMainApi(libs.kotlinTestJUnit) + commonMainApi(libs.mokoTest) +} diff --git a/mvvm-state-test/src/androidMain/AndroidManifest.xml b/mvvm-state-test/src/androidMain/AndroidManifest.xml new file mode 100755 index 00000000..70ab0cf7 --- /dev/null +++ b/mvvm-state-test/src/androidMain/AndroidManifest.xml @@ -0,0 +1,2 @@ + + \ No newline at end of file diff --git a/mvvm-state-test/src/commonMain/kotlin/dev/icerock/moko/mvvm/test/flow/FlowTest.kt b/mvvm-state-test/src/commonMain/kotlin/dev/icerock/moko/mvvm/test/flow/FlowTest.kt new file mode 100644 index 00000000..6917f1f9 --- /dev/null +++ b/mvvm-state-test/src/commonMain/kotlin/dev/icerock/moko/mvvm/test/flow/FlowTest.kt @@ -0,0 +1,139 @@ +/* + * Copyright 2020 IceRock MAG Inc. Use of this source code is governed by the Apache 2.0 license. + */ + +package dev.icerock.moko.mvvm.test.flow + +import dev.icerock.moko.mvvm.flow.dataTransform +import dev.icerock.moko.mvvm.state.ResourceState +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.ExperimentalCoroutinesApi +import kotlinx.coroutines.flow.MutableStateFlow +import kotlinx.coroutines.flow.SharingStarted +import kotlinx.coroutines.flow.StateFlow +import kotlinx.coroutines.flow.combine +import kotlinx.coroutines.flow.stateIn +import kotlinx.coroutines.test.runTest +import kotlin.test.Test +import kotlin.test.assertEquals +import kotlin.test.assertTrue + +@OptIn(ExperimentalCoroutinesApi::class) +open class FlowTest { + + private val coroutineScope = CoroutineScope(Dispatchers.Unconfined) + + @Test + open fun dataTransformTest() = runTest { + val flow: MutableStateFlow> = + MutableStateFlow(ResourceState.Success(10)) + val flowBool: MutableStateFlow = MutableStateFlow(false) + + var dataTransformCounter = 0 + var mergeWithCounter = 0 + + val mapFlow: StateFlow> = flow.dataTransform { + dataTransformCounter++ + combine(this, flowBool) { a1, a2 -> + mergeWithCounter++ + if (a2) a1.toLong() else -a1.toLong() + } + }.stateIn(coroutineScope, SharingStarted.Eagerly, ResourceState.Loading()) + + assertEquals(actual = dataTransformCounter, expected = 1) + assertEquals(actual = mergeWithCounter, expected = 1) + assertEquals(expected = -10, actual = mapFlow.value.dataValue()) + + flowBool.value = true + + assertEquals(actual = dataTransformCounter, expected = 1) + assertEquals(actual = mergeWithCounter, expected = 2) + assertEquals(expected = 10, actual = mapFlow.value.dataValue()) + + flow.value = ResourceState.Success(11) + + assertEquals(actual = dataTransformCounter, expected = 2) + assertEquals(actual = mergeWithCounter, expected = 3) + assertEquals(expected = 11, actual = mapFlow.value.dataValue()) + + flowBool.value = false + + assertEquals(actual = dataTransformCounter, expected = 2) + assertEquals( + actual = mergeWithCounter, + expected = 4 + ) + assertEquals(expected = -11, actual = mapFlow.value.dataValue()) + } + + @Test + open fun dataTransformMergeWithTest() = runTest { + val coroutineScope = CoroutineScope(Dispatchers.Unconfined) + + val vmIsAuthorized = MutableStateFlow(true) + val state: MutableStateFlow> = + MutableStateFlow(ResourceState.Empty()) + val isLoading: MutableStateFlow = MutableStateFlow(false) + + var dataTransformCounter = 0 + var mergeWithDataTransformCounter = 0 + var mergeWithIsAuthorizedCounter = 0 + + val dataTransform: StateFlow> = state.dataTransform { + dataTransformCounter++ + combine(this, isLoading) { a1, a2 -> + mergeWithDataTransformCounter++ + if (a2) a1.toLong() else -a1.toLong() + } + }.stateIn(coroutineScope, SharingStarted.Eagerly, ResourceState.Loading()) + + val result: StateFlow> = combine( + vmIsAuthorized, + dataTransform + ) { isAuthorized, dataState -> + mergeWithIsAuthorizedCounter++ + if (isAuthorized) { + dataState + } else { + ResourceState.Failed(Exception()) + } + }.stateIn(coroutineScope, SharingStarted.Eagerly, ResourceState.Loading()) + + assertEquals(actual = dataTransformCounter, expected = 0) + assertEquals(actual = mergeWithDataTransformCounter, expected = 0) + assertEquals(actual = mergeWithIsAuthorizedCounter, expected = 1) + assertTrue { result.value.isEmpty() } + + state.value = ResourceState.Loading() + + assertEquals(actual = dataTransformCounter, expected = 0) + assertEquals(actual = mergeWithDataTransformCounter, expected = 0) + assertEquals(actual = mergeWithIsAuthorizedCounter, expected = 2) + assertTrue { result.value.isLoading() } + + state.value = ResourceState.Success(10) + + assertEquals(actual = dataTransformCounter, expected = 1) + assertEquals(actual = mergeWithDataTransformCounter, expected = 1) + assertEquals(actual = mergeWithIsAuthorizedCounter, expected = 3) + assertTrue { result.value.isSuccess() } + assertEquals(actual = result.value.dataValue(), expected = -10) + + isLoading.value = true + + assertEquals(actual = dataTransformCounter, expected = 1) + assertEquals(actual = mergeWithDataTransformCounter, expected = 2) + assertEquals(actual = mergeWithIsAuthorizedCounter, expected = 4) + assertTrue { result.value.isSuccess() } + assertEquals(actual = result.value.dataValue(), expected = 10) + + isLoading.value = false + + assertEquals(actual = dataTransformCounter, expected = 1) + assertEquals(actual = mergeWithDataTransformCounter, expected = 3) + assertEquals(actual = mergeWithIsAuthorizedCounter, expected = 5) + assertTrue { result.value.isSuccess() } + assertEquals(actual = result.value.dataValue(), expected = -10) + } +} diff --git a/mvvm-state/src/commonTest/kotlin/dev/icerock/moko/mvvm/livedata/LiveDataTest.kt b/mvvm-state-test/src/commonMain/kotlin/dev/icerock/moko/mvvm/test/livedata/LiveDataTest.kt similarity index 80% rename from mvvm-state/src/commonTest/kotlin/dev/icerock/moko/mvvm/livedata/LiveDataTest.kt rename to mvvm-state-test/src/commonMain/kotlin/dev/icerock/moko/mvvm/test/livedata/LiveDataTest.kt index e0f59b3d..fc756f5b 100644 --- a/mvvm-state/src/commonTest/kotlin/dev/icerock/moko/mvvm/livedata/LiveDataTest.kt +++ b/mvvm-state-test/src/commonMain/kotlin/dev/icerock/moko/mvvm/test/livedata/LiveDataTest.kt @@ -2,22 +2,21 @@ * Copyright 2020 IceRock MAG Inc. Use of this source code is governed by the Apache 2.0 license. */ -package dev.icerock.moko.mvvm.livedata +package dev.icerock.moko.mvvm.test.livedata -import dev.icerock.moko.mvvm.ResourceState -import dev.icerock.moko.test.AndroidArchitectureInstantTaskExecutorRule -import dev.icerock.moko.test.TestRule +import dev.icerock.moko.mvvm.livedata.LiveData +import dev.icerock.moko.mvvm.livedata.MutableLiveData +import dev.icerock.moko.mvvm.livedata.dataTransform +import dev.icerock.moko.mvvm.livedata.mediatorOf +import dev.icerock.moko.mvvm.state.ResourceState import kotlin.test.Test import kotlin.test.assertEquals import kotlin.test.assertTrue -class LiveDataTest { - - @get:TestRule - val instantTaskExecutorRule = AndroidArchitectureInstantTaskExecutorRule() +open class LiveDataTest { @Test - fun dataTransformTest() { + open fun dataTransformTest() { val ld: MutableLiveData> = MutableLiveData(ResourceState.Success(10)) val ldBool: MutableLiveData = MutableLiveData(false) @@ -25,13 +24,14 @@ class LiveDataTest { var dataTransformCounter = 0 var mergeWithCounter = 0 - val mapLd: LiveData> = ld.dataTransform { - dataTransformCounter++ - mediatorOf(this, ldBool) { a1, a2 -> - mergeWithCounter++ - if (a2) a1.toLong() else -a1.toLong() + val mapLd: LiveData> = + ld.dataTransform { + dataTransformCounter++ + mediatorOf(this, ldBool) { a1, a2 -> + mergeWithCounter++ + if (a2) a1.toLong() else -a1.toLong() + } } - } assertEquals(actual = dataTransformCounter, expected = 1) assertEquals(actual = mergeWithCounter, expected = 3) @@ -55,12 +55,12 @@ class LiveDataTest { assertEquals( actual = mergeWithCounter, expected = 9 - ) // FIXME: there's an extra mergeWith lambda call + ) assertEquals(expected = -11, actual = mapLd.value.dataValue()) } @Test - fun dataTransformMergeWithTest() { + open fun dataTransformMergeWithTest() { val vmIsAuthorized = MutableLiveData(true) val state: MutableLiveData> = MutableLiveData(ResourceState.Empty()) @@ -70,13 +70,14 @@ class LiveDataTest { var mergeWithDataTransformCounter = 0 var mergeWithIsAuthorizedCounter = 0 - val dataTransform: LiveData> = state.dataTransform { - dataTransformCounter++ - mediatorOf(this, isLoading) { a1, a2 -> - mergeWithDataTransformCounter++ - if (a2) a1.toLong() else -a1.toLong() + val dataTransform: LiveData> = + state.dataTransform { + dataTransformCounter++ + mediatorOf(this, isLoading) { a1, a2 -> + mergeWithDataTransformCounter++ + if (a2) a1.toLong() else -a1.toLong() + } } - } val result: LiveData> = mediatorOf( vmIsAuthorized, diff --git a/mvvm-state/src/commonMain/kotlin/dev/icerock/moko/mvvm/ResourceState.kt b/mvvm-state/src/commonMain/kotlin/dev/icerock/moko/mvvm/ResourceState.kt deleted file mode 100644 index a6a93278..00000000 --- a/mvvm-state/src/commonMain/kotlin/dev/icerock/moko/mvvm/ResourceState.kt +++ /dev/null @@ -1,21 +0,0 @@ -/* - * Copyright 2019 IceRock MAG Inc. Use of this source code is governed by the Apache 2.0 license. - */ - -package dev.icerock.moko.mvvm - -sealed class ResourceState { - data class Success(val data: T) : ResourceState() - data class Failed(val error: E) : ResourceState() - class Loading : ResourceState() - class Empty : ResourceState() - - fun isLoading(): Boolean = this is Loading - fun isSuccess(): Boolean = this is Success - fun isEmpty(): Boolean = this is Empty - fun isFailed(): Boolean = this is Failed - - fun dataValue(): T? = (this as? Success)?.data - - fun errorValue(): E? = (this as? Failed)?.error -} diff --git a/mvvm-state/src/commonMain/kotlin/dev/icerock/moko/mvvm/livedata/StateLiveDataMerges.kt b/mvvm-state/src/commonMain/kotlin/dev/icerock/moko/mvvm/livedata/StateLiveDataMerges.kt deleted file mode 100644 index eee3b8ed..00000000 --- a/mvvm-state/src/commonMain/kotlin/dev/icerock/moko/mvvm/livedata/StateLiveDataMerges.kt +++ /dev/null @@ -1,29 +0,0 @@ -/* - * Copyright 2021 IceRock MAG Inc. Use of this source code is governed by the Apache 2.0 license. - */ - -package dev.icerock.moko.mvvm.livedata - -import dev.icerock.moko.mvvm.ResourceState - -fun LiveData>.concatData( - liveData: LiveData>, - function: (T1, T2) -> OT -): LiveData> = - mediatorOf(this, liveData) { firstState, secondState -> - val state: ResourceState = when { - (firstState is ResourceState.Loading || secondState is ResourceState.Loading) -> ResourceState.Loading() - (firstState is ResourceState.Failed) -> ResourceState.Failed(firstState.error) - (secondState is ResourceState.Failed) -> ResourceState.Failed(secondState.error) - (firstState is ResourceState.Empty || secondState is ResourceState.Empty) -> ResourceState.Empty() - (firstState is ResourceState.Success && secondState is ResourceState.Success) -> ResourceState.Success( - function( - firstState.data, - secondState.data - ) - ) - else -> ResourceState.Empty() - } - - state - } diff --git a/settings.gradle.kts b/settings.gradle.kts index f201041e..8f9a6451 100644 --- a/settings.gradle.kts +++ b/settings.gradle.kts @@ -27,7 +27,10 @@ include(":mvvm-livedata-swiperefresh") include(":mvvm-livedata-compose") include(":mvvm-databinding") include(":mvvm-viewbinding") -include(":mvvm-state") +include(":mvvm-state-core") +include(":mvvm-state-livedata") +include(":mvvm-state-flow") +include(":mvvm-state-test") include(":mvvm-test") include(":sample:android-app") include(":sample:mpp-library")