-
Notifications
You must be signed in to change notification settings - Fork 4
Transitions
Transitions between states are triggered by events and conditionalized via guard conditions.
In order to transition between states, you'll need to dispatch events. Very often you'll need some metadata to go along with your events so that you can guard some transitions or apply some side effects.
To create an Automata event, all you'll have to do is to extend the abstract Event
class, and you're free to add any data that you want.
class Search extends AutomataEvent {
final String query;
const Search(this.query);
}
On the following example, both the transition defined in the Off
and On
states are simple transitions.
final machine = StateMachine.create(
(g) => g
..initial<Off>()
..state<Off>(builder: (g) => g..on<OnToggle, On>())
..state<On>(builder: (g) => g..on<OnToggle, Off>())
);
The API to define a transition:
void on<E extends AutomataEvent, Target extends AutomataState>({
TransitionType type,
GuardCondition<E>? condition,
List<Action<E>>? actions,
});
On the following example we have a simple transition for the OnAwardPoints
event, this transition will move the state to itself which will then trigger the Eventless transitions created with the always
function. These transitions are executed every time regardless of the event that triggered it, they require a condition and will transition to the given target if it evaluates to true.
final machine = StateMachine.create(
(g) => g
..initial<Playing>()
..state<Playing>(
builder: (b) => b
// Eventless transition
// Will transition to either 'win' or 'lose' immediately upon
// entering 'playing' state or receiving OnAwardPoints event
// if the condition is met.
..always<Win>(condition: (_) => scoreboard.points > 99)
..always<Lose>(condition: (_) => scoreboard.points < 0)
..on<OnAwardPoints, Playing>(
actions: [
(OnAwardPoints e) {
scoreboard.points += e.points;
},
],
),
)
..state<Win>(type: StateNodeType.terminal)
..state<Lose>(type: StateNodeType.terminal),
);
The API to define a eventless transition:
void always<Target extends AutomataState>({
GuardCondition<NullEvent>? condition,
List<Action<NullEvent>>? actions,
});
To trigger a transition you'll dispatch a event:
machine.send(Event());
A dispatched event will produce a change in the state machine's value if any of the active states have a transition defined to react to the given event and if its condition evaluates to true.
Guards are the conditions defined in each transition. A transition is only applied if its guard evaluates to true.
In the following example, the first transition will match the first 18 calls, afterwards, that transition will evaluate as false and the applied transition will be the one that has MiddleAged
as its target.
final machine = StateMachine.create(
(g) => g
..initial<Alive>()
..state<Alive>(
builder: (b) => b
..initial<Young>()
// Transitions
..on<OnBirthday, Young>(
condition: (e) => human.age < 18,
actions: [(e) { human.age++; }],
)
..on<OnBirthday, MiddleAged>(
condition: (e) => human.age < 50,
actions: [(e) { human.age++;}],
)
..on<OnBirthday, Old>(
condition: (e) => human.age < 80,
actions: [(e) { human.age++; }],
)
..on<OnDeath, Purgatory>()
// States
..state<Young>()
..state<MiddleAged>()
..state<Old>(),
)
..state<Dead>(),
);
Actions are fire-and-forget effects. They can be declared in three ways:
- entry actions are executed upon entering a state
- exit actions are executed upon exiting a state
- transition actions are executed when a transition is taken
For more details, check XState's awesome docs, we might not support every case they do, but we aim to be as close as possible:
You can define a list of actions to be applied in order.
final machine = StateMachine.create(
(g) => g
..initial<Solid>()
..state<Solid>(
builder: (b) => b
..on<OnMelted, Liquid>(
actions: [
(e) => effects.add('sideeffect_1'),
(e) => effects.add('sideeffect_2'),
],
),
)
..state<Liquid>(),
);
The API to define a action:
typedef Action<E extends AutomataEvent> = void Function(E event);
OnEnty
and OnExit
are callbacks to be called when a state is entered or exited. First, all nodes exit, and then they are entered from the least common compound ancestor until the leaves.
In the following example when transitioning from Off to On, the Off
's onExit
callback will be called, and then the On
's onEntry
.
final machine = StateMachine.create(
(g) => g
..initial<Off>()
..state<Off>(
builder: (b) => b
..on<OnToggle, On>()
..onEntry((event) {...})
..onExit((event) {...})),
)
..state<On>(
builder: (b) => b
..on<OnToggle, Off>()
..onEntry((event) {...})
..onExit((event) {...})),
)
);
The API to define onEntry and onExit callbacks:
typedef OnEntryAction = void Function(AutomataEvent? event);
typedef OnExitAction = void Function(AutomataEvent? event);
You can listen to all transitions by adding an onTransition
function to the StateMachine.create
call.
final machine = StateMachine.create(
(g) => g
..initial<Solid>()
..state<Solid>(
builder: (b) => b..on<OnMelted, Liquid>(),
)
..state<Liquid>(),
onTransition: (event, state) => transitions.add(event),
);
The API to define a onTransition callback:
typedef OnTransitionCallback = void Function(
AutomataEvent e,
StateMachineValue value,
);
```