fsm2 0.8.1 fsm2: ^0.8.1 copied to clipboard
FMS2 provides an implementation of the core design aspects of the UML state diagrams allowing both declarative transitions and dynamic transitions along with Guard Conditions.
FMS2 provides an implementation of the core design aspects of the UML state diagrams.
Derived from the FSM library which in turn was inspired by Tinder StateMachine library.
State Machines transitions can be defined both declaratively and procedurally.
Guard Conditions from the UML 2 specification are also supported.
FSM2 uses a builder to delcare each state machine.
Your application may declare as many statemachines as necessary.
Delcarative State Transitions #
import 'package:fsm2/fsm2.dart';
void main() {
final machine = StateMachine.create((g) => g
..initialState(Solid())
..state<Solid>((b) => b
..on<OnMelted>((s, e) => b.transitionTo(Liquid())))
));
The above examples creates a Finite State Machine (machine) which delcares its initial state as being Solid
and then declares a single
transition which occurs when the event OnMelted
event is triggered causing a transition to a new state Liquid
.
To trigger an event:
machine.transition(OnMelted());
After the above call to transition machine.currentState
will return Liquid()
.
Guard conditions #
FSM2 supports guard conditions which only allow an event to cause a transition if the Event or State meets some condition.
Guard conditions allow you to declare (via the on
builder) the same Event multiple times from a single State.
When registring multiple events of the same type only a single event may have an empty guard condition and it MUST
be the last event added to the state.
If a State has the same event registered multiple times then the transitions will be evaluated in order. The first transition whose guard condition returns true will be triggered. No further guard conditions will be evaluated. A transition without a Guard Condtions always evaluates to true.
import 'package:fsm2/fsm2.dart';
void main() {
final machine = StateMachine.create((g) => g
..initialState(Solid())
..state<Solid>((b) => b
..on<OnHeat>((s,e) => b.transitionTo(Liquid())
condition: s.temperature + e.deltaDegrees > 0)
..on<OnHeat>((s,e) => b.transitionTo(Boiling())
condition: s.temperature + e.deltaDegrees > 100)
..on<OnMelted>((s, e) => b.transitionTo(Liquid())))
));
You can see from the above example that the OnHeat
contains a field deltaDegrees
. It is often useful to pass
arguments to your events which can then be applied to the State. To pass a value into an event.
machine.transition(OnHeat(deltaDegrees: 25));
Side Effects #
FSM2 allows you to specify side effects for a transition. The side effect is a lambda which will be called when the transition is triggered. A transition that fails to pass its guard condition will not be triggered and its side effect will not be called.
void main() {
final machine = StateMachine.create((g) => g
..initialState(Solid())
..state<Solid>((b) => b
..on<OnHeat>((s,e) => b.transitionTo(Liquid()
, sideEffect: () => print('I melted')),
condition: s.temperature + e.deltaDegrees > 0)
));
Procedural transitions #
Declarative state transitions make it easy to understand a State Machine and all of its transitions. However sometimes delcaritive transitions aren't expresive enough to fully capture the transition logic.
As such FSM2 allows you to procedurally code a transition.
Firstly lets take note of the fact that the following call (taken from the above example) doesn't actually perform a transition.
b.transitionTo(Liquid())
Rather the call returns a Transition
object which the StateMachine uses to determine the transition.
To understand how this works lets look at the on
method in more detail.
The on
method is a lambda of type EventHandler
typedef EventHandler<S extends State, E extends Event> = Transition Function(S s, E e);
The EventHanlder is passed the current State (s) and the Event (e) registered in the on
clause.
..on<OnHeat>((s,e) => b.transitionTo(Liquid())
The event handler is called when the declared event is passed to machine.transition(OnHeat())
. The EventHandler MUST
return a Transition
object which is then used by the StateMachine to transition to the new State defined in the
Transition
object.
The above code creates a Transition object by calling:
b.transitionTo(Liquid());
class Transition {
final State toState;
final SideEffect sideEffect;
}
As you can see the Transition class also takes a SideEffect
which is a lambda that will be called after the
current State's onExit
method is called but before the new State's onEntry
method is called.
To pass a SideEffect to the transition object:
b.transitionTo(OnHeat(deltaDegrees: 60), sideEffect: () => print('new temp is ${s.currentTemp + e.deltaDegrees}'));
We now understand how the EventHandler
and transitionTo
methods work so lets look at how we use these
to procedurally declare a transition.
final machine = StateMachine.create((g) => g
..initialState(Solid())
..state<Solid>((b) => b
..on<OnHeat>((s,e) => heatThingsUp(b, s,e))
....
Transition heatThingsUp(StateBuilder b, Solid s, OnHeat e)
{
var newTemp = s.currentTemp + e.deltaDegrees;
if (newTemp < 0)
return b.transition(OnFroze(newTemp));
else if (newTemp < 100)
return b.transition(OnLiquid(newTemp));
else
return b.transition(OnBoiling(newTemp), sideEffect: () => print('Time for tea'))
}
onEnter/onExit #
The onEnter/onExit methods allow you to define actions that are peformed when we enter or leave a State. It doesn't matter what event caused the new State.
An example might be calculating the State's pressure. It doesn't matter why we entered a State, the state must
always refect its current temperature so using an onEnter
method makes this simple.
Example #
A simple example showing the life cycle of H2O.
import 'package:fsm/fsm.dart';
void main() {
final machine = StateMachine.create((g) => g
..initialState(Solid())
..state<Solid>((b) => b
..on<OnMelted>((s, e) => b.transitionTo(
Liquid(),
sideEffect: () => print('Melted'),
)))
..state<Liquid>((b) => b
..onEnter((s, e) => print('Entering ${s.runtimeType} State'))
..onExit((s, e) => print('Exiting ${s.runtimeType} State'))
..on<OnFroze>((s, e) => b.transitionTo(
Solid(),
sideEffect: () => print('Frozen'),
))
..on<OnVaporized>((s, e) => b.transitionTo(
Gas(),
sideEffect: () => print('Vaporized'),
)))
..state<Gas>((b) => b
..on<OnCondensed>((s, e) => b.transitionTo(
Liquid(),
sideEffect: () => print('Condensed'),
)))
..onTransition((t) => print(
'Recieved Event ${t.event.runtimeType} in State ${t.fromState.runtimeType} transitioning to State ${t.toState.runtimeType}')));
print(machine.currentState is Solid); // TRUE
machine.transition(OnMelted());
print(machine.currentState is Liquid); // TRUE
machine.transition(OnFroze());
print(machine.currentState is Solid); // TRUE
}
Example classes #
The above examples use the following classes.
class Solid implements State {}
class Liquid implements State {}
class Gas implements State {}
class OnMelted implements Event {}
class OnFroze implements Event {}
class OnVaporized implements Event {}
class OnCondensed implements Event {}
class OnHeat implements Event {
int deltaDegrees;
OnHeat({this.deltaDegrees})
}
Features and bugs #
Please file feature requests and bugs at the issue tracker.