Skip to content

Transitions

Ricardo Canastro edited this page Apr 29, 2022 · 14 revisions

Transitions between states are triggered by events and conditionalized via guard conditions.

Defining events and transitions

Create events

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);
}

Simple transitions

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,
});

Eventless transitions

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,
});

Triggering a transitions

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

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

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:

actions

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);

onEntry & onExit

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);

Listen to all transitions

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,
);
```