Skip to content

StateReserve

Abhi Muktheeswarar edited this page Aug 2, 2021 · 8 revisions

The StateReserve holds the State.

class StateReserve<S : State>(
    val config: StateReserveConfig,
    private val initialState: InitialState<S>,
    private val reduce: Reduce<S>,
    middlewares: List<Middleware<S>>?,
) {..}

StateReserve

Internally, the StateReserve contains a StateMachine which holds the current State. This StateMachine is based on the actor model which in turn is based on the concept of

"Do not communicate by sharing memory; instead, share memory by communicating."

The StateReserve uses Channel to communicate with the StateMachine. By doing so, the State is confined to a particular thread, which satisfies the 1st rule "Mutable state == 1 thread" and 2nd rule "Immutable state == many threads" in Kotlin Native Concurrency.

The StateReserve provides the State changes as a Flow<State>.

It has five functions:

  • dispatch(action: Action) function, through which we send Action.
  • getState() function provides access to State synchronously.
  • awaitState() suspend function provides the State after processing all the pending Actions through a reducer.
  • restoreState() To restore the initial state, after recovering from a failure or process death. The initial state can be restored only once.
  • terminate() cancels the scope of the StateReserve.

The following flows are provided by StateReserve

  • actions: Flow<Action> - Broadcasts the Action as soon as they reach the dispatch function, before the Action going through Middleware and reduce.
  • actionStates: Flow<ActionState.Always<Action, S> - Broadcasts the Action and State after it passed through Middleware and reduce function. It will be broadcasted even if the Action doesn't change the State.
  • transitions: Flow<Any> - This flow can be collected by using the provided extension functions to listen for state transitions when enhancedStateMachine config is enabled.

StateReserveConfig

class StateReserveConfig(
    val scope: CoroutineScope,
    val debugMode: Boolean,
    val ignoreDuplicateState: Boolean = true,
    val enhancedStateMachine: Boolean = false,
    val assertStateValues: Boolean = debugMode,
    val checkMutableState: Boolean = debugMode,
)
  • scope: This is the scope for the StateReserve and its SideEffect(s). We recommend to provide a CoroutineScope with Dispatchers.Default + SupervisorJob() + CoroutineExceptionHandler.
  • debugMode: If true, checks if the reducer is pure.
  • ignoreDuplicateState: Set this to false only when using Flywheel as a direct dependency in non-JVM platforms where the equals() and hashCode() may not work with Kotlin. This issue happens when defining State in Objective-C.
  • enhancedStateMachine: To use StateReserve as a proper StateMachine supporting listeners for state transitions.
  • assertStateValues: Just in case if you want to do the reducer check even when debug mode is false.
  • checkMutableState: Checks if the State is mutated.

Please note: assertStateValues and checkMutableState doesn't any check in non-JVM platforms.

Unlike Redux, where the entire app has one store, the StateReserve can be scoped to the entire application, to a screen, to a particular feature or even we can define one app-level StateReserve and a StateReserve for each screen. In Android and Apple platforms, we recommended having a StateReserve for each screen. Optionally, you can also have an app-level StateReserve for uses cases like managing a user's authentication state, configurations, etc. There is no restriction on how to scope the StateReserve. Make sure to call terminate() when the StateReserve is not required anymore.

StateReserve as a proper StateMachine

Flywheel helps you to build a proper state machine to an extent. To use Flywheel's StateReserve as a state machine, set enhancedStateMachine = true in the StateReserveConfig.

State reducer

One of the important rules of a state machine is that it should not allow invalid state transition. To explain this let's see an example:

val reducer =
    reducerForAction<MaterialAction, MaterialState> { action, state ->

        when (action) {
            MaterialAction.OnMelted -> MaterialState.Liquid
            MaterialAction.OnFrozen -> MaterialState.Solid
            MaterialAction.OnVaporized -> MaterialState.Gas
            MaterialAction.OnCondensed -> MaterialState.Liquid
        }
    }

In the above reducer we change the State, i.e transition to another State based on the received Action alone. We are not taking the current State into account while transitioning. Here we are not defining any invalid transitions. For example, a solid cannot transition to gas without changing to a liquid first. Assuming the current state is solid if we send MaterialAction.OnVaporized action, it will transition to gas. But it's incorrect. It should not be allowed.
To restrict such invalid transitions, we have to take the current State into account.

val reducer =
    reducerForAction<MaterialAction, MaterialState> { action, state ->

        when (state) {
            MaterialState.Solid -> when (action) {
                MaterialAction.OnMelted -> MaterialState.Liquid
                else -> reduceError()
            }
            MaterialState.Liquid -> when (action) {
                MaterialAction.OnFrozen -> MaterialState.Solid
                MaterialAction.OnVaporized -> MaterialState.Gas
                else -> reduceError()
            }
            MaterialState.Gas -> when (action) {
                MaterialAction.OnCondensed -> MaterialState.Liquid
                else -> reduceError()
            }
        }
    }

In the updated reducer, we first check the current State and then the Action. if no match is found we simply throw an error. So if you send an action MaterialAction.OnVaporized it will transition only when the current State is liquid. This way, we can enforce invalid state transition which satisfies one of the state machine behaviour. reduceError() function is provided for this use-case to report invalid transition.

State transitions

Flywheel supports listening for state transitions by collecting the transitions: Flow<Any> in StateReserve along with the provided extension functions.

  • specificStates<S>() - For collecting only specific parts of a State. This is useful when you need to collect a subset of the State for a view component.
data class(val items: List<Any>, val selectedItems: List<Any>)

states.specificStates { it.selectedItems }.collect{..}
  • validTransitions<From, To>() - For collecting only when valid transitions happen. Example:
transitions.validTransitions<MaterialState.Solid, MaterialState.Liquid>()
  • inValidTransition<State>() - For collecting when a invalid transition was attempted for a Action.
transitions.inValidTransition<MaterialState>().collect{..}
  • onEnter<State>() - For collecting when a specificState instance is entered.
transitions.onEnter<MaterialState.Liquid>().collect{..}
  • onExit<State>() - For collecting when a specificState instance is exited.
transitions.onExit<MaterialState.Liquid>().collect{..}
  • specificActions<Action>() - For collecting Action of specific instance types. For example, you might want to only collect Action of type NavigateAction to deal with navigation-related stuff.

Please note Flywheel's StateReserve does not adhere to the W3C SCXML specification.

Clone this wiki locally