From e753cd30ad39a0e9b83f064c08fa1b0dd3c9b33a Mon Sep 17 00:00:00 2001 From: Adam Brown Date: Wed, 2 Nov 2022 09:39:55 +0000 Subject: [PATCH] fix state emissions becoming out of sync when using async handlers --- .../kotlin/app/dapk/st/core/StateViewModel.kt | 3 +-- .../src/main/kotlin/app/dapk/state/State.kt | 26 ++++++++++++++----- .../testFixtures/kotlin/test/ReducerTest.kt | 2 +- 3 files changed, 21 insertions(+), 10 deletions(-) diff --git a/domains/android/compose-core/src/main/kotlin/app/dapk/st/core/StateViewModel.kt b/domains/android/compose-core/src/main/kotlin/app/dapk/st/core/StateViewModel.kt index 2c01997..a584b8a 100644 --- a/domains/android/compose-core/src/main/kotlin/app/dapk/st/core/StateViewModel.kt +++ b/domains/android/compose-core/src/main/kotlin/app/dapk/st/core/StateViewModel.kt @@ -11,7 +11,6 @@ import app.dapk.state.Store import app.dapk.state.createStore import kotlinx.coroutines.flow.MutableSharedFlow import kotlinx.coroutines.flow.SharedFlow -import kotlinx.coroutines.launch class StateViewModel( reducerFactory: ReducerFactory, @@ -32,7 +31,7 @@ class StateViewModel( } override fun dispatch(action: Action) { - viewModelScope.launch { store.dispatch(action) } + store.dispatch(action) } } diff --git a/domains/state/src/main/kotlin/app/dapk/state/State.kt b/domains/state/src/main/kotlin/app/dapk/state/State.kt index d65dfcf..777137f 100644 --- a/domains/state/src/main/kotlin/app/dapk/state/State.kt +++ b/domains/state/src/main/kotlin/app/dapk/state/State.kt @@ -4,18 +4,26 @@ import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.launch import kotlin.reflect.KClass -fun createStore(reducerFactory: ReducerFactory, coroutineScope: CoroutineScope): Store { +fun createStore(reducerFactory: ReducerFactory, coroutineScope: CoroutineScope, log: Boolean = false): Store { val subscribers = mutableListOf<(S) -> Unit>() var state: S = reducerFactory.initialState() return object : Store { private val scope = createScope(coroutineScope, this) private val reducer = reducerFactory.create(scope) - override suspend fun dispatch(action: Action) { - scope.coroutineScope.launch { + override fun dispatch(action: Action) { + coroutineScope.launch { + if (log) println("??? dispatch $action") state = reducer.reduce(action).also { nextState -> if (nextState != state) { + if (log) { + println("??? action: $action result...") + println("??? current: $state") + println("??? next: $nextState") + } subscribers.forEach { it.invoke(nextState) } + } else { + if (log) println("??? action: $action skipped, no change") } } } @@ -35,7 +43,7 @@ interface ReducerFactory { } fun interface Reducer { - suspend fun reduce(action: Action): S + fun reduce(action: Action): S } private fun createScope(coroutineScope: CoroutineScope, store: Store) = object : ReducerScope { @@ -45,7 +53,7 @@ private fun createScope(coroutineScope: CoroutineScope, store: Store) = o } interface Store { - suspend fun dispatch(action: Action) + fun dispatch(action: Action) fun getState(): S fun subscribe(subscriber: (S) -> Unit) } @@ -82,14 +90,18 @@ fun createReducer( actionHandlers.fold(acc) { acc, handler -> when (handler) { is ActionHandler.Async -> { - handler.handler.invoke(scope, action) + scope.coroutineScope.launch { + handler.handler.invoke(scope, action) + } acc } is ActionHandler.Sync -> handler.handler.invoke(action, acc) is ActionHandler.Delegate -> when (val next = handler.handler.invoke(scope, action)) { is ActionHandler.Async -> { - next.handler.invoke(scope, action) + scope.coroutineScope.launch { + next.handler.invoke(scope, action) + } acc } diff --git a/domains/state/src/testFixtures/kotlin/test/ReducerTest.kt b/domains/state/src/testFixtures/kotlin/test/ReducerTest.kt index 3fdf94a..950c7d1 100644 --- a/domains/state/src/testFixtures/kotlin/test/ReducerTest.kt +++ b/domains/state/src/testFixtures/kotlin/test/ReducerTest.kt @@ -53,7 +53,7 @@ class ReducerTestScope( } private val reducer: Reducer = reducerFactory.create(reducerScope) - override suspend fun reduce(action: Action) = reducer.reduce(action).also { + override fun reduce(action: Action) = reducer.reduce(action).also { capturedResult = it }