From a328d50a637d84e54f29b4c2cb052ad4a10ac98c Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Wed, 26 Jul 2023 22:34:50 +0200 Subject: [PATCH 1/3] Update dependency androidx.recyclerview:recyclerview to v1.3.1 (#545) Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com> --- gradle/libs.versions.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index 55c438af..f15acb58 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -22,7 +22,7 @@ androidx-constraintlayout = "2.1.4" androidx-core = "1.10.1" androidx-lifecycle = "2.6.1" androidx-fragment = "1.5.2" -androidx-recyclerview = "1.3.0" +androidx-recyclerview = "1.3.1" material = "1.9.0" adapterdelegates = "4.3.2" From b03cd44d8c8786ad186a83fa6f05ba75fd064b4a Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Thu, 27 Jul 2023 07:59:11 +0200 Subject: [PATCH 2/3] Update dependency androidx.compose.compiler:compiler to v1.5.1 (#544) Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com> Co-authored-by: Hannes Dorfmann --- gradle/libs.versions.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index f15acb58..9d9ec098 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -13,7 +13,7 @@ android-target = "33" android-compile = "33" androidx-activity = "1.7.2" androidx-appcompat = "1.6.1" -androidx-compose-compiler = "1.5.0" +androidx-compose-compiler = "1.5.1" androidx-compose-runtime = "1.4.3" androidx-compose-ui = "1.4.3" androidx-compose-foundation = "1.4.3" From 372474ae9467c0f2908bce6fa9b9255b1d253cca Mon Sep 17 00:00:00 2001 From: Gabriel Ittner Date: Thu, 27 Jul 2023 18:46:45 +0200 Subject: [PATCH 3/3] allow identity to be null (#547) --- .../flowredux/dsl/ConditionBuilderBlock.kt | 2 +- .../flowredux/dsl/IdentityBuilderBlock.kt | 2 +- .../flowredux/dsl/InStateBuilderBlock.kt | 2 +- .../freeletics/flowredux/StateAndAction.kt | 2 + .../flowredux/dsl/IdentityBlockTest.kt | 200 ++++++++++++++++++ 5 files changed, 205 insertions(+), 3 deletions(-) diff --git a/flowredux/src/commonMain/kotlin/com/freeletics/flowredux/dsl/ConditionBuilderBlock.kt b/flowredux/src/commonMain/kotlin/com/freeletics/flowredux/dsl/ConditionBuilderBlock.kt index 97dc05de..9bf5e2f0 100644 --- a/flowredux/src/commonMain/kotlin/com/freeletics/flowredux/dsl/ConditionBuilderBlock.kt +++ b/flowredux/src/commonMain/kotlin/com/freeletics/flowredux/dsl/ConditionBuilderBlock.kt @@ -18,7 +18,7 @@ public class ConditionBuilderBlock internal co * Afterwards a the blocks are started again for the new `identity`. */ public fun untilIdentityChanges( - identity: (InputState) -> Any, + identity: (InputState) -> Any?, block: IdentityBuilderBlock.() -> Unit, ) { sideEffectBuilders += IdentityBuilderBlock(isInState, identity) diff --git a/flowredux/src/commonMain/kotlin/com/freeletics/flowredux/dsl/IdentityBuilderBlock.kt b/flowredux/src/commonMain/kotlin/com/freeletics/flowredux/dsl/IdentityBuilderBlock.kt index 56f34cf6..a35eea56 100644 --- a/flowredux/src/commonMain/kotlin/com/freeletics/flowredux/dsl/IdentityBuilderBlock.kt +++ b/flowredux/src/commonMain/kotlin/com/freeletics/flowredux/dsl/IdentityBuilderBlock.kt @@ -8,7 +8,7 @@ import kotlinx.coroutines.ExperimentalCoroutinesApi @FlowReduxDsl public class IdentityBuilderBlock internal constructor( override val isInState: SideEffectBuilder.IsInState, - private val identity: (InputState) -> Any, + private val identity: (InputState) -> Any?, ) : BaseBuilderBlock() { @Suppress("UNCHECKED_CAST") diff --git a/flowredux/src/commonMain/kotlin/com/freeletics/flowredux/dsl/InStateBuilderBlock.kt b/flowredux/src/commonMain/kotlin/com/freeletics/flowredux/dsl/InStateBuilderBlock.kt index 710574af..95abbefb 100644 --- a/flowredux/src/commonMain/kotlin/com/freeletics/flowredux/dsl/InStateBuilderBlock.kt +++ b/flowredux/src/commonMain/kotlin/com/freeletics/flowredux/dsl/InStateBuilderBlock.kt @@ -32,7 +32,7 @@ public class InStateBuilderBlock internal cons * Afterwards a the blocks are started again for the new `identity`. */ public fun untilIdentityChanges( - identity: (InputState) -> Any, + identity: (InputState) -> Any?, block: IdentityBuilderBlock.() -> Unit, ) { sideEffectBuilders += IdentityBuilderBlock(isInState, identity) diff --git a/flowredux/src/commonTest/kotlin/com/freeletics/flowredux/StateAndAction.kt b/flowredux/src/commonTest/kotlin/com/freeletics/flowredux/StateAndAction.kt index 68f66869..7321d582 100644 --- a/flowredux/src/commonTest/kotlin/com/freeletics/flowredux/StateAndAction.kt +++ b/flowredux/src/commonTest/kotlin/com/freeletics/flowredux/StateAndAction.kt @@ -15,5 +15,7 @@ internal sealed class TestState { data class GenericState(val aString: String, val anInt: Int) : TestState() + data class GenericNullableState(val aString: String?, val anInt: Int?) : TestState() + data class CounterState(val counter: Int) : TestState() } diff --git a/flowredux/src/commonTest/kotlin/com/freeletics/flowredux/dsl/IdentityBlockTest.kt b/flowredux/src/commonTest/kotlin/com/freeletics/flowredux/dsl/IdentityBlockTest.kt index ed686418..0c6bd016 100644 --- a/flowredux/src/commonTest/kotlin/com/freeletics/flowredux/dsl/IdentityBlockTest.kt +++ b/flowredux/src/commonTest/kotlin/com/freeletics/flowredux/dsl/IdentityBlockTest.kt @@ -215,4 +215,204 @@ internal class IdentityBlockTest { assertEquals(1, cancellations[0].first) assertIsNot(cancellations[0].second) } + + @Test + fun blockStartsWhenIdentityChangesBetweenNullAndNotNull() = runTest { + var counter = 0 + + val gs1 = TestState.GenericNullableState(null, null) + + val sm = StateMachine { + inState { + on { _, state -> + state.override { gs1 } + } + } + + inState { + untilIdentityChanges({ it.anInt }) { + onEnterEffect { + counter++ + } + } + + on { _, state -> + state.mutate { copy(anInt = (anInt ?: 0) + 1) } + } + + on { _, state -> + state.mutate { copy(anInt = null) } + } + } + } + + sm.state.test { + assertEquals(TestState.Initial, awaitItem()) + sm.dispatchAsync(TestAction.A1) + assertEquals(gs1, awaitItem()) + sm.dispatchAsync(TestAction.A1) + assertEquals(gs1.copy(anInt = 1), awaitItem()) + sm.dispatchAsync(TestAction.A2) + assertEquals(gs1, awaitItem()) + } + + assertEquals(3, counter) + } + + @Test + fun blockDoesNotStartAgainIfIdentityStaysNull() = runTest { + var counter = 0 + + val gs1 = TestState.GenericNullableState(null, null) + + val sm = StateMachine { + inState { + on { _, state -> + state.override { gs1 } + } + } + + inState { + untilIdentityChanges({ it.anInt }) { + onEnterEffect { + counter++ + } + } + + on { _, state -> + state.mutate { copy(aString = aString + "1") } + } + } + } + + sm.state.test { + assertEquals(TestState.Initial, awaitItem()) + sm.dispatchAsync(TestAction.A1) + assertEquals(gs1, awaitItem()) + sm.dispatchAsync(TestAction.A1) + assertEquals(gs1.copy(aString = "null1"), awaitItem()) + sm.dispatchAsync(TestAction.A1) + assertEquals(gs1.copy(aString = "null11"), awaitItem()) + sm.dispatchAsync(TestAction.A1) + assertEquals(gs1.copy(aString = "null111"), awaitItem()) + sm.dispatchAsync(TestAction.A1) + assertEquals(gs1.copy(aString = "null1111"), awaitItem()) + } + + assertEquals(1, counter) + } + + @Test + fun blockIsCancelledIfIdentityChangesBetweenNullAndNotNull() = runTest { + val cancellations = mutableListOf>() + + val gs1 = TestState.GenericNullableState(null, null) + + val sm = StateMachine { + inState { + on { _, state -> + state.override { gs1 } + } + } + + inState { + untilIdentityChanges({ it.anInt }) { + onEnter { + try { + awaitCancellation() + } catch (t: Throwable) { + cancellations.add(it.snapshot.anInt to t) + throw t + } + } + } + + on { _, state -> + state.mutate { copy(anInt = (anInt ?: 0) + 1) } + } + } + } + + sm.state.test { + assertEquals(TestState.Initial, awaitItem()) + sm.dispatchAsync(TestAction.A1) + assertEquals(gs1, awaitItem()) + sm.dispatchAsync(TestAction.A1) + assertEquals(gs1.copy(anInt = 1), awaitItem()) + sm.dispatchAsync(TestAction.A1) + assertEquals(gs1.copy(anInt = 2), awaitItem()) + sm.dispatchAsync(TestAction.A1) + assertEquals(gs1.copy(anInt = 3), awaitItem()) + sm.dispatchAsync(TestAction.A1) + assertEquals(gs1.copy(anInt = 4), awaitItem()) + + assertEquals(4, cancellations.size) + } + + assertEquals(5, cancellations.size) + assertEquals(null, cancellations[0].first) + assertIs(cancellations[0].second) + assertEquals(1, cancellations[1].first) + assertIs(cancellations[1].second) + assertEquals(2, cancellations[2].first) + assertIs(cancellations[2].second) + assertEquals(3, cancellations[3].first) + assertIs(cancellations[3].second) + // this last cancellation comes when the state machine shuts down + assertEquals(4, cancellations[4].first) + assertIsNot(cancellations[4].second) + } + + @Test + fun blockIsNotCancelledIfIdentityStaysNull() = runTest { + val cancellations = mutableListOf>() + + val gs1 = TestState.GenericNullableState(null, null) + + val sm = StateMachine { + inState { + on { _, state -> + state.override { gs1 } + } + } + + inState { + untilIdentityChanges({ it.anInt }) { + onEnter { + try { + awaitCancellation() + } catch (t: Throwable) { + cancellations.add(it.snapshot.anInt to t) + throw t + } + } + } + + on { _, state -> + state.mutate { copy(aString = aString + "1") } + } + } + } + + sm.state.test { + assertEquals(TestState.Initial, awaitItem()) + sm.dispatchAsync(TestAction.A1) + assertEquals(gs1, awaitItem()) + sm.dispatchAsync(TestAction.A1) + assertEquals(gs1.copy(aString = "null1"), awaitItem()) + sm.dispatchAsync(TestAction.A1) + assertEquals(gs1.copy(aString = "null11"), awaitItem()) + sm.dispatchAsync(TestAction.A1) + assertEquals(gs1.copy(aString = "null111"), awaitItem()) + sm.dispatchAsync(TestAction.A1) + assertEquals(gs1.copy(aString = "null1111"), awaitItem()) + + assertEquals(0, cancellations.size) + } + + assertEquals(1, cancellations.size) + // this cancellation comes when the state machine shuts down + assertEquals(null, cancellations[0].first) + assertIsNot(cancellations[0].second) + } }