Replies: 4 comments 5 replies
-
Hi @doverdho The described behavior (all actions are broadcasted everywhere) is by design. With that said, I am wondering what the problem exactly is that you are struggling with (and therefore your suggestion is coming from)? Do you have a real-world example where your motivation is coming from? What is exactly the problem you try to solve? |
Beta Was this translation helpful? Give feedback.
-
Hi @sockeqwe , My team has identified the following two use cases for only hitting the most fitting entry:
While 2 is considered a nice to have within our team, the first use case is heavily used in other non-Kotlin products that we have, therefore it would make it easier for us if this was available in FlowReduxStateMachine. I've attached a small sample project with the following scenario: An event that is known to the state is dispatched, so we expect that handler to execute and trigger the state change. What we also see is that the base state handling for that specific event is called, which calls the queueEvent handler. We also notice that the unhandledEvent handler is also called, resulting in the following logs:
Sometimes I notice that the ServiceState is not entered, as the logging is not made. I can't explain this, as I thought that the ExecutionPolicy.Ordered would make sure that the full handling is done before another handler is called. |
Beta Was this translation helpful? Give feedback.
-
I think that goes against the design of a state machine in general (not unique to FlowRedux). I don't think we will support something like this in FlowRedux. To me, it sounds like you have to revisit either your design choice: why do you need to queue events to get started with? maybe your state machine is not properly designed? If an event is important enough to be queued while your state machine is in a certain state, then maybe that state should handle that event. Handling this "event" in that state means to me reacting to it: reacting to it could mean also something like having a
How do we know what is the "most fitting entry"? it is impossible to know just from source code and DSL definition, correct?
State Machines by definition can "ignore" events where there is not handler / state transition designed for. I see that it could potentially be also a workaround for the "queue" issue that you have described above. If you want to write a proposal how to implement an "unhandled" action / event solution in FlowRedux, we are open to discussing this or what do you think @gabrielittner ? But for me personally, this "feature" has low priority. Regarding your test example: Do you mind creating an "Issue" out of it as it seems to be unrelated to the proposal you are making here but rather a bug in FlowRedux or missing coverage in the documentation? I will only have time to look at this though somwhen mid next week. But if it is related to
the With ExcecutionPolicy. UNORDERED on the same example above, FlowRedux would not ensure any ordering of execution, thus the second A1's The important bit though is that this ordering / or not ordering is only for the |
Beta Was this translation helpful? Give feedback.
-
I've played a bit with the idea of only calling the most important handler and it's generally possible to build this on top of the existing API. This is what I got and based on the TODOs I think it's hard for us to build this in a generic way into the library. Like Hannes mentioned above we don't really know how to prioritize things. This adds an @OptIn(ExperimentalCoroutinesApi::class)
inline fun <reified S : Any, reified A: Any> FlowReduxStoreBuilder<S, A>.inStateExclusive(block: InStateExclusiveBuilder<S, A, S, A>.() -> Unit) {
val builder = InStateExclusiveBuilder<S, A, S, A>(S::class).apply(block)
val handlers = builder.build()
// TODO: does not handle deeper type hierarchies because sealedSubclasses only returns direct subclasses
// so this would probably need to be recursive but that would also make the handler prioritization harder
// because someone could have a fallback handler in the intermediate state class
S::class.sealedSubclasses.forEach { subState ->
A::class.sealedSubclasses.forEach { subAction ->
val handler = handlers[subState to subAction] ?:
// fall back to base action type first in sub state
handlers[subState to A::class] ?:
// fall back to concrete action in base state
// TODO: it's not clear whether this one or the one above should take precedence
handlers[S::class to subAction] ?:
// fall back to concrete action in base state
handlers[S::class to A::class]
// the if could alternatively be a checkNotNull to enforce having a handler
if (handler != null) {
@Suppress("invisible_member") // the internal API is marked with @PublishedApi so it's safe to call
inState(subState) {
on(subAction, ExecutionPolicy.CANCEL_PREVIOUS) { action, state ->
@Suppress("UNCHECKED_CAST") // this is safe
handler(action, state as State<S>)
}
}
}
}
}
}
class InStateExclusiveBuilder<BaseS: Any, BaseA : Any, S : BaseS, A : BaseA>(val stateType: KClass<S>) {
val actionHandlers = mutableMapOf<Pair<KClass<out BaseS>, KClass<out BaseA>>, (BaseA, State<BaseS>) -> ChangedState<BaseS>> ()
// TODO: needs all other dsl function
inline fun <reified SubA: A> on(noinline handler: (SubA, State<S>) -> ChangedState<BaseS>) {
@Suppress("UNCHECKED_CAST")
actionHandlers[stateType to SubA::class] = handler as Function2<BaseA, State<BaseS>, ChangedState<BaseS>>
}
// nesting blocks for subclasses of subclasses
inline fun <reified SubS : S> inSubStateExclusive(block: InStateExclusiveBuilder<BaseS, BaseA, SubS, A>.() -> Unit) {
val builder = InStateExclusiveBuilder<BaseS, BaseA, SubS, A>(SubS::class).apply(block)
actionHandlers.putAll(builder.build())
}
fun build() = actionHandlers.toMap()
} Example usage: sealed interface BaseState
data object State1 : BaseState
data object State2 : BaseState
data object State3 : BaseState
sealed interface BaseAction
data object A : BaseAction
data object B : BaseAction
data object C : BaseAction
data object ChangeToState2 : BaseAction
data object ChangeToState3 : BaseAction
@OptIn(ExperimentalCoroutinesApi::class)
class MyStateMachine : FlowReduxStateMachine<BaseState, BaseAction>(State1) {
init {
spec {
inStateExclusive<BaseState, BaseAction> {
on<BaseAction> { action, state ->
println("BaseState, BaseAction handler received $action")
state.noChange()
}
on<A> { action, state ->
println("BaseState, A handler received $action")
state.noChange()
}
on<B> { action, state ->
println("BaseState, B handler received $action")
state.noChange()
}
inSubStateExclusive<State1> {
on<A> { action, state ->
println("State1, A handler received $action")
state.noChange()
}
on<ChangeToState2> { _, state ->
state.override { State2 }
}
}
inSubStateExclusive<State2> {
on<B> { action, state ->
println("State2, B handler received $action")
state.noChange()
}
on<ChangeToState3> { _, state ->
state.override { State3 }
}
}
}
}
}
}
fun main() = runBlocking {
val sm = MyStateMachine()
sm.state.test {
println("SM in state ${awaitItem()}")
sm.dispatch(A)
sm.dispatch(B)
sm.dispatch(C)
sm.dispatch(ChangeToState2)
println("SM in state ${awaitItem()}")
sm.dispatch(A)
sm.dispatch(B)
sm.dispatch(C)
sm.dispatch(ChangeToState3)
println("SM in state ${awaitItem()}")
sm.dispatch(A)
sm.dispatch(B)
sm.dispatch(C)
}
} The above gives this output
Because of the mentioned complexity/uncertainty above it might be best to just build a solution like this on top of the existing DSL. That way you can make sure it fits the specific needs of your use case. |
Beta Was this translation helpful? Give feedback.
-
Hi,
I have a statemachine where I have events that are only handled in certain states. I've written handlers for these states, but it may be possible that these events are received outside of their intended state. In that case, I would like to log this event, so that if this occurs we can check what needs to be done. I've added the inState (with all states deriving from BaseState) and added an event handler that is able to handle my BaseEvent. I've noticed now that all my events are handled twice, once in the current state and once in the BaseState handler. This seems quite strange to me, my assumption was that the state machine implementation would pick the best match instead of all matches. Can this be considered as a feature?
Beta Was this translation helpful? Give feedback.
All reactions